├── .commitlintrc.json ├── .eslintrc ├── .gitattributes ├── .github ├── CODEOWNERS ├── actions │ ├── cleanup-tests │ │ └── action.yml │ └── prepare-tests │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release.yml │ └── weekly-master-beta-sync.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── .typedoc ├── summary-generator.cjs └── typedoc-frontmatter-theme.cjs ├── LICENSE ├── README.md ├── cypress.config.ts ├── docs ├── CONCEPTS.md ├── CONTRIBUTING.md ├── FEATURES.md ├── PLUGINS.md ├── TESTING.md ├── UPGRADING.md └── USAGE.md ├── examples ├── .gitignore ├── core-api │ ├── manual-pools.ts │ ├── override-module.ts │ ├── scan.ts │ └── step-by-step.ts ├── docs-examples │ └── quickstarts │ │ ├── quickstart │ │ └── requestor.mjs │ │ └── retrievable-task │ │ └── task.mjs ├── experimental │ ├── deployment │ │ └── new-api.ts │ ├── express │ │ ├── public │ │ │ └── .gitkeep │ │ └── server.ts │ └── job │ │ ├── cancel.ts │ │ ├── getJobById.ts │ │ └── waitForResults.ts ├── package.json ├── rental-model │ ├── advanced │ │ ├── deposit │ │ │ ├── config.ts │ │ │ ├── contracts │ │ │ │ ├── glmAbi.json │ │ │ │ └── lockAbi.json │ │ │ ├── funder.ts │ │ │ ├── index.ts │ │ │ └── observer.ts │ │ ├── gpu-ai.ts │ │ ├── gpu │ │ │ ├── bandwidthTest │ │ │ └── gpu.ts │ │ ├── local-image │ │ │ ├── Dockerfile │ │ │ ├── alpine.gvmi │ │ │ └── local-image.ts │ │ ├── outbound │ │ │ └── whitelist │ │ │ │ ├── manifest.json │ │ │ │ └── read-golem-js-releases.ts │ │ ├── payment-filters.ts │ │ ├── proposal-filter.ts │ │ ├── proposal-predefined-filter.ts │ │ ├── proposal-selector.ts │ │ ├── reuse-allocation.ts │ │ ├── setup-and-teardown.ts │ │ └── tcp-proxy │ │ │ ├── server.js │ │ │ └── tcp-proxy.ts │ └── basic │ │ ├── events.ts │ │ ├── many-of.ts │ │ ├── one-of.ts │ │ ├── run-and-stream.ts │ │ ├── transfer.ts │ │ └── vpn.ts ├── tsconfig.json └── web │ ├── hello.html │ └── transfer-data.html ├── jest.config.json ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── activity │ ├── activity.module.ts │ ├── activity.test.ts │ ├── activity.ts │ ├── api.ts │ ├── config.ts │ ├── exe-script-executor.test.ts │ ├── exe-script-executor.ts │ ├── exe-unit │ │ ├── batch.spec.ts │ │ ├── batch.ts │ │ ├── error.ts │ │ ├── exe-unit.test.ts │ │ ├── exe-unit.ts │ │ ├── index.ts │ │ ├── process.spec.ts │ │ └── process.ts │ ├── index.ts │ ├── results.test.ts │ ├── results.ts │ └── script │ │ ├── command.ts │ │ ├── index.ts │ │ └── script.ts ├── experimental │ ├── README.md │ ├── deployment │ │ ├── builder.test.ts │ │ ├── builder.ts │ │ ├── deployment.ts │ │ ├── index.ts │ │ └── validate-deployment.ts │ ├── index.ts │ ├── job │ │ ├── index.ts │ │ ├── job.test.ts │ │ ├── job.ts │ │ └── job_manager.ts │ └── reputation │ │ ├── error.ts │ │ ├── index.ts │ │ ├── system.ts │ │ └── types.ts ├── golem-network │ ├── golem-network.test.ts │ ├── golem-network.ts │ ├── index.ts │ └── plugin.ts ├── index.ts ├── market │ ├── agreement │ │ ├── agreement-event.ts │ │ ├── agreement.test.ts │ │ ├── agreement.ts │ │ └── index.ts │ ├── api.ts │ ├── demand │ │ ├── demand-body-builder.test.ts │ │ ├── demand-body-builder.ts │ │ ├── demand.ts │ │ ├── directors │ │ │ ├── base-config.ts │ │ │ ├── basic-demand-director-config.test.ts │ │ │ ├── basic-demand-director-config.ts │ │ │ ├── basic-demand-director.ts │ │ │ ├── payment-demand-director-config.test.ts │ │ │ ├── payment-demand-director-config.ts │ │ │ ├── payment-demand-director.ts │ │ │ ├── workload-demand-director-config.ts │ │ │ ├── workload-demand-director.test.ts │ │ │ └── workload-demand-director.ts │ │ ├── index.ts │ │ └── options.ts │ ├── draft-offer-proposal-pool.test.ts │ ├── draft-offer-proposal-pool.ts │ ├── error.ts │ ├── helpers.spec.ts │ ├── helpers.ts │ ├── index.ts │ ├── market.module.test.ts │ ├── market.module.ts │ ├── proposal │ │ ├── index.ts │ │ ├── market-proposal-event.ts │ │ ├── market-proposal.ts │ │ ├── offer-counter-proposal.ts │ │ ├── offer-proposal.test.ts │ │ ├── offer-proposal.ts │ │ ├── proposal-properties.ts │ │ ├── proposals_batch.test.ts │ │ └── proposals_batch.ts │ ├── scan │ │ ├── index.ts │ │ ├── scan-director.ts │ │ ├── scanned-offer.test.ts │ │ ├── scanned-offer.ts │ │ └── types.ts │ ├── strategy.test.ts │ └── strategy.ts ├── network │ ├── api.ts │ ├── error.ts │ ├── index.ts │ ├── network.module.test.ts │ ├── network.module.ts │ ├── network.test.ts │ ├── network.ts │ ├── node.ts │ ├── tcp-proxy.test.ts │ └── tcp-proxy.ts ├── payment │ ├── BaseDocument.ts │ ├── InvoiceProcessor.ts │ ├── PayerDetails.ts │ ├── agreement_payment_process.spec.ts │ ├── agreement_payment_process.ts │ ├── allocation.test.ts │ ├── allocation.ts │ ├── api.ts │ ├── config.ts │ ├── debit_note.spec.ts │ ├── debit_note.ts │ ├── error.ts │ ├── index.ts │ ├── invoice.spec.ts │ ├── invoice.ts │ ├── payment.module.ts │ ├── rejection.ts │ ├── service.ts │ ├── strategy.test.ts │ └── strategy.ts ├── resource-rental │ ├── index.ts │ ├── rental.module.ts │ ├── resource-rental-pool.test.ts │ ├── resource-rental-pool.ts │ ├── resource-rental.test.ts │ └── resource-rental.ts └── shared │ ├── cache │ └── CacheService.ts │ ├── error │ └── golem-error.ts │ ├── storage │ ├── StorageServerAdapter.ts │ ├── default.ts │ ├── gftp.ts │ ├── index.ts │ ├── null.ts │ ├── provider.ts │ ├── ws.test.ts │ └── ws.ts │ ├── types.ts │ ├── utils │ ├── abortSignal.ts │ ├── acquireQueue.ts │ ├── apiErrorMessage.ts │ ├── env.test.ts │ ├── env.ts │ ├── eventLoop.ts │ ├── index.ts │ ├── logger │ │ ├── defaultLogger.ts │ │ ├── logger.ts │ │ └── nullLogger.ts │ ├── runtimeContextChecker.ts │ ├── rxjs.ts │ ├── sleep.ts │ ├── timeout.ts │ ├── types.ts │ └── wait.ts │ └── yagna │ ├── adapters │ ├── activity-api-adapter.ts │ ├── index.ts │ ├── market-api-adapter.test.ts │ ├── market-api-adapter.ts │ ├── network-api-adapter.ts │ └── payment-api-adapter.ts │ ├── event-reader.ts │ ├── index.ts │ ├── repository │ ├── activity-repository.ts │ ├── agreement-repository.ts │ ├── debit-note-repository.ts │ ├── demand-repository.ts │ ├── index.ts │ ├── invoice-repository.ts │ └── proposal-repository.ts │ ├── yagna.spec.ts │ └── yagnaApi.ts ├── tests ├── cypress │ ├── support │ │ ├── commands.ts │ │ └── e2e.ts │ ├── tsconfig.json │ └── ui │ │ ├── hello-world.cy.ts │ │ └── transfer-data.cy.ts ├── docker │ ├── Provider.Dockerfile │ ├── Requestor.Dockerfile │ ├── configureProvider.py │ ├── data-node │ │ └── ya-provider │ │ │ ├── globals.json │ │ │ ├── hardware.json │ │ │ └── presets.json │ ├── docker-compose.yml │ ├── fundRequestor.sh │ └── startRequestor.sh ├── e2e │ ├── _setupEnv.ts │ ├── _setupLogging.ts │ ├── express.spec.ts │ ├── jest.config.json │ └── resourceRentalPool.spec.ts ├── examples │ ├── examples.json │ └── examples.test.ts ├── fixtures │ ├── alpine.gvmi │ └── cubes.blend ├── import │ ├── import.test.mjs │ ├── jest.config.js │ └── require.test.cjs ├── jest.config.json ├── jest.d.ts ├── jest.setup.ts ├── tsconfig.json └── utils │ ├── error_matcher.ts │ └── helpers.ts ├── tsconfig.json └── tsconfig.spec.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "shared-node-browser": true 6 | }, 7 | "extends": ["eslint:recommended", "prettier"], 8 | "rules": { 9 | "no-constant-condition": [ 10 | "error", 11 | { 12 | "checkLoops": false 13 | } 14 | ], 15 | "@typescript-eslint/naming-convention": [ 16 | "error", 17 | { 18 | "selector": "import", 19 | "format": ["camelCase", "PascalCase"] 20 | }, 21 | { 22 | "selector": "variable", 23 | "format": ["camelCase", "UPPER_CASE"], 24 | "leadingUnderscore": "forbid", 25 | "trailingUnderscore": "forbid" 26 | }, 27 | { 28 | "selector": ["memberLike", "function"], 29 | "format": ["camelCase"], 30 | "leadingUnderscore": "forbid", 31 | "filter": { 32 | "regex": "golem.*", // ProposalProperties like 'golem.com.payment.debit-notes.accept-timeout?' 33 | "match": false 34 | } 35 | }, 36 | { 37 | "selector": "typeLike", 38 | "format": ["PascalCase"] 39 | }, 40 | { 41 | "selector": "enumMember", 42 | "format": ["PascalCase"] 43 | }, 44 | { 45 | "selector": "objectLiteralProperty", 46 | "format": null // We have too many varrying cases like YAGNA_APPKEY, Authorization, golem.com.scheme.payu.payment-timeout-sec? which break this constantly 47 | } 48 | ] 49 | }, 50 | "overrides": [ 51 | { 52 | "files": ["**/*.ts"], 53 | "extends": ["plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"], 54 | "plugins": ["@typescript-eslint"] 55 | }, 56 | { 57 | "files": ["**/*.js"], 58 | "extends": ["eslint:recommended"] 59 | } 60 | ], 61 | "ignorePatterns": ["dist/", "handbook_gen/", "docs/", "bundle.js", "tests", "*.config.ts", "tmp/"] 62 | } 63 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @golemfactory/ya-sdk 2 | -------------------------------------------------------------------------------- /.github/actions/cleanup-tests/action.yml: -------------------------------------------------------------------------------- 1 | name: "Cleanup test environment" 2 | description: "Cleanup test environment" 3 | inputs: 4 | type: 5 | description: "Type of test" 6 | required: true 7 | default: "test" 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Collect logs from providers and requestor 12 | shell: bash 13 | if: always() 14 | run: | 15 | mkdir log-output 16 | docker compose -f tests/docker/docker-compose.yml logs provider-1 > log-output/${{inputs.type}}-provider-1.log 17 | docker compose -f tests/docker/docker-compose.yml logs provider-2 > log-output/${{inputs.type}}-provider-2.log 18 | docker compose -f tests/docker/docker-compose.yml logs requestor > log-output/${{inputs.type}}-requestor.log 19 | 20 | - name: Upload provider output and logs 21 | uses: actions/upload-artifact@v4 22 | if: always() 23 | with: 24 | name: ${{inputs.type}}-golem-provider-and-requestor-logs 25 | path: log-output 26 | 27 | - name: Cleanup Docker 28 | shell: bash 29 | if: always() 30 | run: | 31 | c=$(docker ps -q) 32 | [[ $c ]] && docker kill $c 33 | docker system prune -af 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | target-branch: "beta" 11 | schedule: 12 | interval: "weekly" 13 | groups: 14 | dev-deps-regular: 15 | dependency-type: "development" 16 | update-types: 17 | - "minor" 18 | - "patch" 19 | prod-deps-regular: 20 | dependency-type: "production" 21 | update-types: 22 | - "minor" 23 | - "patch" 24 | dev-deps-breaking: 25 | dependency-type: "development" 26 | update-types: 27 | - "major" 28 | prod-deps-breaking: 29 | dependency-type: "production" 30 | update-types: 31 | - "major" 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Regular CI Pipeline 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | # Regular release channels 7 | - master 8 | - next 9 | - beta 10 | - alpha 11 | # Support, hotfix branches like: 1.0.x or 1.x 12 | - '([0-9]+)(\.([0-9]+))?\.x' 13 | 14 | # Allows triggering the workflow manually 15 | workflow_dispatch: 16 | 17 | jobs: 18 | regular-checks: 19 | name: Build and unit-test on supported platforms and NodeJS versions 20 | strategy: 21 | matrix: 22 | # Make sure you're addressing it to the minor version, as sometimes macos was picking 20.9 while others 20.10 23 | # and that caused issues with rollup 24 | node-version: [18.20.x, 20.18.x, 22.x] 25 | os: [ubuntu-latest, windows-latest, macos-latest] 26 | 27 | runs-on: ${{ matrix.os }} 28 | timeout-minutes: 10 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Setup NodeJS ${{ matrix.node-version }} 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | 38 | - name: Perform regular checks 39 | run: | 40 | npm ci 41 | npm run format:check 42 | npm run lint 43 | npm run test:unit 44 | npm run build 45 | npm install --prefix examples 46 | npm run --prefix examples lint:ts 47 | npm run test:import 48 | -------------------------------------------------------------------------------- /.github/workflows/weekly-master-beta-sync.yml: -------------------------------------------------------------------------------- 1 | name: Weekly master to beta sync 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # Do it on Wednesday :) 6 | - cron: "0 0 * * 3" 7 | jobs: 8 | sync: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout files 12 | uses: actions/checkout@v4 13 | 14 | - name: Create pull request 15 | uses: peter-evans/create-pull-request@v6 16 | with: 17 | commit-message: "chore: sync master to beta" 18 | branch: sync/master-to-beta 19 | delete-branch: true 20 | base: beta 21 | title: "chore: sync master to beta" 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Sensitive details 2 | .env 3 | 4 | #Node modules 5 | node_modules/ 6 | 7 | #Outputs 8 | dist/ 9 | coverage/ 10 | /.nyc_output 11 | /examples/blender/*.png 12 | /examples/yacat/*.sh 13 | /examples/yacat/*.txt 14 | /examples/yacat/*.hash 15 | /examples/yacat/*.potfile 16 | api-reference/ 17 | logs/ 18 | .cypress 19 | 20 | #Lock files 21 | *.lock 22 | 23 | #IDE 24 | .idea 25 | *.DS_Store 26 | .vscode 27 | 28 | #Nuxt 29 | /examples/adv-web-example/.nuxt 30 | tmp/ 31 | reports/ 32 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run format:check && npm run lint 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.nyc_output 2 | /.vscode 3 | /coverage 4 | /examples 5 | /logs 6 | /node_modules 7 | /src 8 | /tests 9 | /tmp 10 | .github 11 | .gitignore 12 | package-lock.json 13 | tsconfig.json 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.gvmi 3 | tmp/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | "@semantic-release/npm", 6 | [ 7 | "@semantic-release/github", 8 | { 9 | "successComment": false 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.typedoc/summary-generator.cjs: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const fetchTitle = /title: ["'](.*)["']/; 4 | 5 | async function prepareDocAnchor(docsDir, type, file) { 6 | const filePath = path.join(docsDir, type, file); 7 | const fileContent = fs.readFileSync(filePath, "utf-8"); 8 | 9 | if (fetchTitle.test(fileContent)) { 10 | const title = fetchTitle 11 | .exec(fileContent)[1] 12 | .replace(` ${type[0].toUpperCase() + type.slice(1)} `, " ") 13 | .replace(" - golem-js API Reference", "") 14 | .replace(/^(Class|Interface|Module|Enum)\s+/, ""); 15 | 16 | return { 17 | link: `${type}/${file.replace(".md", "")}`, 18 | title, 19 | }; 20 | } 21 | } 22 | 23 | (async () => { 24 | const docsDir = process.argv[2] || "api-reference"; 25 | const directoryPath = path.join(__dirname, "..", docsDir); 26 | const logFilePath = path.join(process.argv[3] || docsDir, "overview.md"); 27 | const logStream = fs.createWriteStream(logFilePath, { flags: "w" }); 28 | 29 | const types = fs 30 | .readdirSync(directoryPath, { withFileTypes: true }) 31 | .filter((i) => i.isDirectory()) 32 | .map((d) => d.name); 33 | 34 | logStream.write(`--- 35 | title: golem-js API reference overview 36 | description: Dive into the content overview of the Golem-JS API reference. 37 | type: JS API Reference 38 | --- 39 | `); 40 | 41 | for (const type of types) { 42 | logStream.write(`\n* ${type[0].toUpperCase() + type.slice(1)}`); 43 | const files = fs.readdirSync(path.join(directoryPath, type), { withFileTypes: true }).map((f) => f.name); 44 | 45 | for (const file of files) { 46 | const doc = await prepareDocAnchor(docsDir, type, file); 47 | if (doc && doc.title) logStream.write(`\n\t* [${doc.title}](${doc.link})`); 48 | } 49 | } 50 | 51 | logStream.end(); 52 | console.info("🪄 GitBook summary successfully generated"); 53 | })().catch((e) => { 54 | fs.appendFileSync("error.log", e.message + "\n"); 55 | }); 56 | -------------------------------------------------------------------------------- /.typedoc/typedoc-frontmatter-theme.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const typedoc = require("typedoc"); 3 | const HugoTheme = require("typedoc-plugin-markdown").MarkdownTheme; 4 | 5 | class ModifiedHugoTheme extends HugoTheme { 6 | constructor(renderer) { 7 | super(renderer); 8 | this.listenTo(this.owner, { [typedoc.PageEvent.END]: this.onHugoPageEnd }); 9 | } 10 | 11 | onHugoPageEnd(page) { 12 | const yamlVars = { 13 | title: `${typedoc.ReflectionKind[page.model.kind]} ${this.getPageTitle(page)}`, 14 | pageTitle: `${typedoc.ReflectionKind[page.model.kind]} ${this.getPageTitle(page)} - golem-js API Reference`, 15 | description: `Explore the detailed API reference documentation for the ${ 16 | typedoc.ReflectionKind[page.model.kind] 17 | } ${this.getPageTitle(page)} within the golem-js SDK for the Golem Network.`, 18 | type: "JS API Reference", 19 | }; 20 | page.contents && (page.contents = this.prependYAML(page.contents, yamlVars)); 21 | } 22 | 23 | getPageTitle(page) { 24 | return page.url === "modules.md" && this.indexTitle ? this.indexTitle : page.model.name; 25 | } 26 | 27 | getSlug(page) { 28 | return (page.url.match(/\/([^\/]*)\.[^.$]*$/) || [, page.url])[1]; 29 | } 30 | 31 | prependYAML(contents, yamlVars) { 32 | return ( 33 | "---\n" + 34 | Object.entries(yamlVars) 35 | .map(([key, value]) => `${key}: "${value}"`) 36 | 37 | .join("\n") + 38 | "\n---\n" + 39 | contents 40 | ); 41 | } 42 | } 43 | 44 | function load({ application }) { 45 | const theme = new ModifiedHugoTheme(application.renderer); 46 | 47 | application.converter.on(typedoc.Converter.EVENT_RESOLVE_BEGIN, (context) => { 48 | for (const reflection of context.project.getReflectionsByKind(typedoc.ReflectionKind.Reference)) { 49 | context.project.removeReflection(reflection); 50 | } 51 | 52 | for (const reflection of context.project.getReflectionsByKind(typedoc.ReflectionKind.Module)) { 53 | if (!reflection.children || !reflection.children.length) { 54 | context.project.removeReflection(reflection); 55 | } 56 | } 57 | }); 58 | } 59 | 60 | exports.load = load; 61 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | fileServerFolder: "examples/web", 5 | supportFolder: "tests/cypress/support", 6 | videosFolder: ".cypress/video", 7 | screenshotsFolder: ".cypress/screenshots", 8 | defaultCommandTimeout: 180000, 9 | experimentalInteractiveRunEvents: true, 10 | chromeWebSecurity: false, 11 | video: true, 12 | e2e: { 13 | baseUrl: "http://localhost:3000", 14 | supportFile: "tests/cypress/support/e2e.ts", 15 | specPattern: "tests/cypress/ui/**/*.cy.ts", 16 | setupNodeEvents(on, config) { 17 | return new Promise(async (res) => { 18 | config.env.YAGNA_APPKEY = process.env.YAGNA_APPKEY; 19 | config.env.YAGNA_API_BASEPATH = process.env.YAGNA_API_URL; 20 | config.env.YAGNA_SUBNET = process.env.YAGNA_SUBNET; 21 | config.env.PAYMENT_NETWORK = process.env.PAYMENT_NETWORK; 22 | res(config); 23 | }); 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You want to contribute to `golem-js`? That's great! This guide will help you get started. 4 | 5 | ## Setup local environment 6 | 7 | 1. Clone this repository 8 | 2. In the root of this project run `npm install` to install all necessary dependencies 9 | 3. To build the SDK run `npm run build` 10 | 4. Install yagna as described in the [README](../README.md) file - you will need it to test your changes against testnet (no real funds will be required to execute workloads on Golem Network) 11 | 12 | ### Unit Testing 13 | 14 | For unit testing `golem-js` uses `jest` with [ts-mockito](https://www.npmjs.com/package/@johanblumenberg/ts-mockito) to mock code. 15 | 16 | You can run tests using: 17 | 18 | ```bash 19 | npm run test:unit 20 | ``` 21 | 22 | The test files are usually co-located with the files that they test in the `src/` folder. 23 | 24 | ### Pre-commit hooks 25 | 26 | We use `husky` to enforce few rules using `prettier`, `eslint` or even commit message format which allows us to use [semantic-release](https://github.com/semantic-release/semantic-release). 27 | 28 | ## Pull Request Guidelines 29 | 30 | Our development revolves around few branches: 31 | 32 | - `master` - contains the latest stable production code (production track) 33 | - `beta` - where the SDK team developers next major releases (slow track) 34 | - `alpha` - when a different major release has to take precedence before `beta` (fast track) 35 | 36 | The process is as follows: 37 | 38 | - Depending on the contribution you're planning to make, create a `feature/`, `bugfix/` branch from the base branch (typically `master`), and merge back against that branch. 39 | - In case of any contribution: 40 | - Make sure you provide proper description for the PR (see template below) 41 | - Add test cases if possible within the same PR 42 | 43 | ### PR description templates 44 | 45 | #### Feature 46 | 47 | ```markdown 48 | ## Why is it needed? 49 | 50 | _Explain why the feature is valuable and what problem does it solve._ 51 | 52 | ## What should be changed? 53 | 54 | _Explain the general idea behind the code changes - high level description of your solution to the problem stated above._ 55 | ``` 56 | 57 | #### Bugfix 58 | 59 | ```markdown 60 | ## Steps to reproduce 61 | 62 | 1. _Do this_ 63 | 2. _Then that_ 64 | 3. _Finally this_ 65 | 66 | ## Expected result 67 | 68 | _Describe the desired outcome (how does fixed look like)_ 69 | 70 | ## Actual result 71 | 72 | _Describe the actual outcome (what happens now)_ 73 | ``` 74 | 75 | ## Discord 76 | 77 | Feel invited to join our [Discord](http://discord.gg/golem). You can meet other SDK users and developers in the `#sdk-discussion` and `#js-discussion` channels. 78 | 79 | ## Thanks 💙 80 | 81 | Thanks for all your contributions and efforts towards improving `golem-js`! 82 | -------------------------------------------------------------------------------- /docs/TESTING.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | This document provides guidance for testing the SDK. 4 | 5 | ## Running unit tests 6 | 7 | To run unit tests, you can simply execute the command: 8 | 9 | ```bash 10 | npm run test:unit 11 | # or 12 | yarn test:unit 13 | ``` 14 | 15 | ## Running E2E tests 16 | 17 | Both test cases for the NodeJS environment and the browser (cypress) require preparation of a test environment of the 18 | Golem Network with Providers and all the necessary infrastructure. 19 | 20 | ### Prerequisites 21 | 22 | 1. Ensure you have `docker` and `docker-compose` installed in your system. 23 | 2. Your Linux environment should have nested virtualization enabled. 24 | 25 | ### Test Environment Preparation 26 | 27 | Follow these steps to prepare your test environment: 28 | 29 | #### Build Docker Containers 30 | 31 | First, build the Docker containers using the `docker-compose.yml` file located under `tests/docker`. 32 | 33 | Execute this command to build the Docker containers: 34 | 35 | docker-compose -f tests/docker/docker-compose.yml build 36 | 37 | #### Start Docker Containers 38 | 39 | Then, launch the Docker containers you've just built using the same `docker-compose.yml` file. 40 | 41 | Execute this command to start the Docker containers: 42 | 43 | docker-compose -f tests/docker/docker-compose.yml down && docker-compose -f tests/docker/docker-compose.yml up -d 44 | 45 | #### Fund the Requestor 46 | 47 | The next step is to fund the requestor. 48 | 49 | docker exec -t docker_requestor_1 /bin/sh -c "/golem-js/tests/docker/fundRequestor.sh" 50 | 51 | ### Install and Build the SDK 52 | 53 | Finally, install and build the golem-js SDK in the Docker container 54 | 55 | Run this chain of commands to install and build the SDK and prepare cypress. 56 | 57 | ```docker 58 | docker exec -t docker_requestor_1 /bin/sh -c "cd /golem-js && npm i && npm run build && ./node_modules/.bin/cypress install" 59 | ``` 60 | 61 | ### Execute the E2E Tests 62 | 63 | With your test environment set up, you can now initiate the E2E tests. Run the following command to start: 64 | 65 | ```docker 66 | docker exec -t docker_requestor_1 /bin/sh -c "cd /golem-js && npm run test:e2e" 67 | ``` 68 | 69 | ### Execute the cypress Tests 70 | 71 | First make sure that the webserver that's used for testing is running, by running the command 72 | 73 | ```docker 74 | docker exec -t -d docker_requestor_1 /bin/sh -c "cd /golem-js/examples/web && node app.mjs" 75 | ``` 76 | 77 | Now you're ready to start the cypress tests by running the command 78 | 79 | ```docker 80 | docker exec -t docker_requestor_1 /bin/sh -c "cd /golem-js && npm run test:cypress -- --browser chromium" 81 | ``` 82 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.bin 2 | package-lock.json -------------------------------------------------------------------------------- /examples/core-api/override-module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * In this advanced example, we will provide our own implementation of one of the core modules 3 | * of the SDK. This example is catered towards library authors who want to extend the SDK's 4 | * functionality or to advanced users who know what they're doing. 5 | * It's **very** easy to break things if you don't have a good understanding of the SDK's internals, 6 | * therefore this feature is not recommended for most users. 7 | */ 8 | 9 | import { MarketOrderSpec, GolemNetwork } from "@golem-sdk/golem-js"; 10 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 11 | 12 | // let's override the `estimateBudget` method from the `MarketModule` interface 13 | // to provide our own implementation 14 | // we need to import the default implementation of the module we want to override 15 | import { MarketModuleImpl } from "@golem-sdk/golem-js"; 16 | 17 | class MyMarketModule extends MarketModuleImpl { 18 | estimateBudget({ maxAgreements, order }: { maxAgreements: number; order: MarketOrderSpec }): number { 19 | // let's take the original estimate and add 20% to it as a buffer 20 | const originalEstimate = super.estimateBudget({ maxAgreements, order }); 21 | return originalEstimate * 1.2; 22 | } 23 | } 24 | 25 | const order: MarketOrderSpec = { 26 | demand: { 27 | workload: { imageTag: "golem/alpine:latest" }, 28 | }, 29 | // based on this order, the "normal" estimateBudget would return 1.5 30 | // (0.5 start price + 0.5 / hour for CPU + 0.5 / hour for env). 31 | // Our override should return 1.8 (1.5 * 1.2) 32 | market: { 33 | rentHours: 1, 34 | pricing: { 35 | model: "linear", 36 | maxStartPrice: 0.5, 37 | maxCpuPerHourPrice: 0.5, 38 | maxEnvPerHourPrice: 0.5, 39 | }, 40 | }, 41 | }; 42 | 43 | (async () => { 44 | const glm = new GolemNetwork({ 45 | logger: pinoPrettyLogger({ 46 | level: "info", 47 | }), 48 | // here's where we provide our own implementation 49 | override: { 50 | market: MyMarketModule, 51 | }, 52 | }); 53 | 54 | // look at the console output to see the budget estimate 55 | glm.payment.events.on("allocationCreated", ({ allocation }) => { 56 | console.log("Allocation created with budget:", Number(allocation.remainingAmount).toFixed(2)); 57 | }); 58 | 59 | try { 60 | await glm.connect(); 61 | const rental = await glm.oneOf({ order }); 62 | await rental 63 | .getExeUnit() 64 | .then((exe) => exe.run("echo Hello, Golem! 👋")) 65 | .then((res) => console.log(res.stdout)); 66 | await rental.stopAndFinalize(); 67 | } catch (err) { 68 | console.error("Failed to run the example", err); 69 | } finally { 70 | await glm.disconnect(); 71 | } 72 | })().catch(console.error); 73 | -------------------------------------------------------------------------------- /examples/core-api/scan.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates how to scan the market for providers that meet specific requirements. 3 | */ 4 | import { GolemNetwork, ScanOptions } from "@golem-sdk/golem-js"; 5 | import { last, map, scan, takeUntil, tap, timer } from "rxjs"; 6 | 7 | // What providers are we looking for? 8 | const scanOptions: ScanOptions = { 9 | // fairly powerful machine but not too powerful 10 | workload: { 11 | runtime: { 12 | name: "vm", 13 | }, 14 | minCpuCores: 4, 15 | maxCpuCores: 16, 16 | minMemGib: 4, 17 | maxMemGib: 8, 18 | capabilities: ["vpn"], 19 | minStorageGib: 16, 20 | }, 21 | // let's look at mainnet providers only 22 | payment: { 23 | network: "polygon", 24 | }, 25 | }; 26 | 27 | (async () => { 28 | const glm = new GolemNetwork(); 29 | await glm.connect(); 30 | const spec = glm.market.buildScanSpecification(scanOptions); 31 | 32 | // For advanced users: you can also add constraints manually: 33 | // spec.constraints.push("(golem.node.id.name=my-favorite-provider)"); 34 | 35 | const SCAN_DURATION_MS = 10_000; 36 | 37 | console.log(`Scanning for ${SCAN_DURATION_MS / 1000} seconds...`); 38 | glm.market 39 | .scan(spec) 40 | .pipe( 41 | tap((scannedOffer) => { 42 | console.log("Found offer from", scannedOffer.provider.name); 43 | }), 44 | // calculate the cost of an hour of work 45 | map( 46 | (scannedOffer) => 47 | scannedOffer.pricing.start + // 48 | scannedOffer.pricing.cpuSec * 3600 + 49 | scannedOffer.pricing.envSec * 3600, 50 | ), 51 | // calculate the running average 52 | scan((total, cost) => total + cost, 0), 53 | map((totalCost, index) => totalCost / (index + 1)), 54 | // stop scanning after SCAN_DURATION_MS 55 | takeUntil(timer(SCAN_DURATION_MS)), 56 | last(), 57 | ) 58 | .subscribe({ 59 | next: (averageCost) => { 60 | console.log("Average cost for an hour of work:", averageCost.toFixed(6), "GLM"); 61 | }, 62 | complete: () => { 63 | console.log("Scan completed, shutting down..."); 64 | glm.disconnect(); 65 | }, 66 | }); 67 | })(); 68 | -------------------------------------------------------------------------------- /examples/docs-examples/quickstarts/quickstart/requestor.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates how easily lease multiple machines at once. 3 | */ 4 | 5 | import { GolemNetwork } from "@golem-sdk/golem-js"; 6 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 7 | 8 | const order = { 9 | demand: { 10 | workload: { imageTag: "golem/alpine:latest" }, 11 | }, 12 | market: { 13 | rentHours: 0.5, 14 | pricing: { 15 | model: "linear", 16 | maxStartPrice: 0.5, 17 | maxCpuPerHourPrice: 1.0, 18 | maxEnvPerHourPrice: 0.5, 19 | }, 20 | }, 21 | }; 22 | 23 | (async () => { 24 | const glm = new GolemNetwork({ 25 | logger: pinoPrettyLogger({ 26 | level: "info", 27 | }), 28 | api: { key: "try_golem" }, 29 | }); 30 | 31 | try { 32 | await glm.connect(); 33 | const pool = await glm.manyOf({ 34 | // I want to have a minimum of one machine in the pool, 35 | // but only a maximum of 3 machines can work at the same time 36 | poolSize: { min: 1, max: 3 }, 37 | order, 38 | }); 39 | // I have 5 parts of the task to perform in parallel 40 | const data = [...Array(5).keys()]; 41 | const results = await Promise.allSettled( 42 | data.map((i) => 43 | pool.withRental((rental) => 44 | rental 45 | .getExeUnit() 46 | .then((exe) => 47 | exe.run( 48 | `echo "Part #${i} computed on provider ${exe.provider.name} with CPU:" && cat /proc/cpuinfo | grep 'model name'`, 49 | ), 50 | ), 51 | ), 52 | ), 53 | ); 54 | results.forEach((result) => { 55 | if (result.status === "fulfilled") { 56 | console.log("Success:", result.value.stdout); 57 | } else { 58 | console.log("Failure:", result.reason); 59 | } 60 | }); 61 | } catch (err) { 62 | console.error("Failed to run the example", err); 63 | } finally { 64 | await glm.disconnect(); 65 | } 66 | })().catch(console.error); 67 | -------------------------------------------------------------------------------- /examples/docs-examples/quickstarts/retrievable-task/task.mjs: -------------------------------------------------------------------------------- 1 | import { GolemNetwork } from "@golem-sdk/golem-js/experimental"; 2 | 3 | const golem = new GolemNetwork({ 4 | yagna: { apiKey: "try_golem" }, 5 | }); 6 | await golem.init(); 7 | 8 | const job = golem.createJob({ 9 | package: { 10 | imageTag: "golem/alpine:latest", 11 | }, 12 | }); 13 | job.startWork(async (exe) => { 14 | const response = await exe.run("echo 'Hello, Golem!'"); 15 | return response.stdout; 16 | }); 17 | 18 | const result = await job.waitForResult(); 19 | 20 | console.log("Job finished with state:", job.state); 21 | console.log("Job results:", result); 22 | 23 | await golem.close(); 24 | -------------------------------------------------------------------------------- /examples/experimental/express/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemfactory/golem-js/76e1342c318b1b62ae0a68fd9b3330999003d4a3/examples/experimental/express/public/.gitkeep -------------------------------------------------------------------------------- /examples/experimental/job/cancel.ts: -------------------------------------------------------------------------------- 1 | import { JobManager } from "@golem-sdk/golem-js/experimental"; 2 | import { MarketOrderSpec } from "@golem-sdk/golem-js"; 3 | const golem = new JobManager({ 4 | api: { 5 | key: "try_golem", 6 | }, 7 | }); 8 | 9 | const order: MarketOrderSpec = { 10 | demand: { 11 | workload: { imageTag: "severyn/espeak:latest" }, 12 | }, 13 | market: { 14 | rentHours: 0.5, 15 | pricing: { 16 | model: "linear", 17 | maxStartPrice: 1, 18 | maxCpuPerHourPrice: 1, 19 | maxEnvPerHourPrice: 1, 20 | }, 21 | }, 22 | }; 23 | 24 | async function main() { 25 | await golem.init(); 26 | 27 | const job = golem.createJob(order); 28 | 29 | console.log("Job object created, initial status is", job.state); 30 | 31 | job.events.addListener("started", () => { 32 | console.log("Job started event emitted"); 33 | }); 34 | job.events.addListener("error", (error) => { 35 | console.log("Job error event emitted with error:", error); 36 | }); 37 | job.events.addListener("canceled", () => { 38 | console.log("Job canceled event emitted"); 39 | }); 40 | job.events.addListener("ended", () => { 41 | console.log("Job ended event emitted"); 42 | }); 43 | 44 | job.startWork(async (exe) => { 45 | return String((await exe.run("echo -n 'Hello Golem!'")).stdout); 46 | }); 47 | 48 | console.log("Canceling job..."); 49 | await job.cancel(); 50 | } 51 | 52 | main() 53 | .catch((error) => { 54 | console.error(error); 55 | process.exitCode = 1; 56 | }) 57 | .finally(async () => { 58 | await golem.close(); 59 | console.log("Golem network closed"); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/experimental/job/getJobById.ts: -------------------------------------------------------------------------------- 1 | import { JobManager } from "@golem-sdk/golem-js/experimental"; 2 | import { MarketOrderSpec } from "@golem-sdk/golem-js"; 3 | 4 | const golem = new JobManager({ 5 | api: { 6 | key: "try_golem", 7 | }, 8 | }); 9 | 10 | const order: MarketOrderSpec = { 11 | demand: { 12 | workload: { imageTag: "severyn/espeak:latest" }, 13 | }, 14 | market: { 15 | rentHours: 0.5, 16 | pricing: { 17 | model: "linear", 18 | maxStartPrice: 1, 19 | maxCpuPerHourPrice: 1, 20 | maxEnvPerHourPrice: 1, 21 | }, 22 | }, 23 | }; 24 | 25 | function startJob() { 26 | const job = golem.createJob(order); 27 | 28 | console.log("Job object created, initial status is", job.state); 29 | 30 | job.events.addListener("started", () => { 31 | console.log("Job started event emitted"); 32 | }); 33 | job.events.addListener("success", () => { 34 | console.log("Job success event emitted"); 35 | }); 36 | job.events.addListener("ended", () => { 37 | console.log("Job ended event emitted"); 38 | }); 39 | 40 | job.startWork(async (exe) => { 41 | return String((await exe.run("echo -n 'Hello Golem!'")).stdout); 42 | }); 43 | return job.id; 44 | } 45 | 46 | async function main() { 47 | await golem.init(); 48 | const jobId = startJob(); 49 | 50 | const jobObject = golem.getJobById(jobId); 51 | const result = await jobObject!.waitForResult(); 52 | 53 | console.log("Job finished, result is ", result); 54 | } 55 | 56 | main() 57 | .catch((error) => { 58 | console.error(error); 59 | process.exitCode = 1; 60 | }) 61 | .finally(async () => { 62 | await golem.close(); 63 | console.log("Golem network closed"); 64 | }); 65 | -------------------------------------------------------------------------------- /examples/experimental/job/waitForResults.ts: -------------------------------------------------------------------------------- 1 | import { JobManager } from "@golem-sdk/golem-js/experimental"; 2 | import { MarketOrderSpec } from "@golem-sdk/golem-js"; 3 | const golem = new JobManager({ 4 | api: { 5 | key: "try_golem", 6 | }, 7 | }); 8 | 9 | const order: MarketOrderSpec = { 10 | demand: { 11 | workload: { 12 | imageTag: "severyn/espeak:latest", 13 | }, 14 | }, 15 | market: { 16 | rentHours: 0.5, 17 | pricing: { 18 | model: "linear", 19 | maxStartPrice: 1, 20 | maxCpuPerHourPrice: 1, 21 | maxEnvPerHourPrice: 1, 22 | }, 23 | }, 24 | }; 25 | 26 | async function main() { 27 | await golem.init(); 28 | 29 | const job = golem.createJob(order); 30 | 31 | console.log("Job object created, initial status is", job.state); 32 | 33 | job.events.addListener("started", () => { 34 | console.log("Job started event emitted"); 35 | }); 36 | job.events.addListener("success", () => { 37 | console.log("Job success event emitted"); 38 | }); 39 | job.events.addListener("ended", () => { 40 | console.log("Job ended event emitted"); 41 | }); 42 | 43 | job.startWork(async (exe) => { 44 | return String((await exe.run("echo -n 'Hello Golem!'")).stdout); 45 | }); 46 | 47 | const result = await job.waitForResult(); 48 | console.log("Job finished, result is ", result); 49 | } 50 | 51 | main() 52 | .catch((error) => { 53 | console.error(error); 54 | process.exitCode = 1; 55 | }) 56 | .finally(async () => { 57 | await golem.close(); 58 | console.log("Golem network closed"); 59 | }); 60 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "golem-js-examples", 3 | "version": "0.0.0", 4 | "description": "NodeJS API Examples for Next Golem", 5 | "type": "module", 6 | "repository": "https://github.com/golemfactory/golem-js", 7 | "scripts": { 8 | "basic-one-of": "tsx rental-model/basic/one-of.ts", 9 | "basic-many-of": "tsx rental-model/basic/many-of.ts", 10 | "basic-vpn": "tsx rental-model/basic/vpn.ts", 11 | "basic-transfer": "tsx rental-model/basic/transfer.ts", 12 | "basic-events": "tsx rental-model/basic/events.ts", 13 | "basic-run-and-stream": "tsx rental-model/basic/run-and-stream.ts", 14 | "advanced-manual-pools": "tsx core-api/manual-pools.ts", 15 | "advanced-step-by-step": "tsx core-api/step-by-step.ts", 16 | "advanced-payment-filters": "tsx rental-model/advanced/payment-filters.ts", 17 | "advanced-proposal-filters": "tsx rental-model/advanced/proposal-filter.ts", 18 | "advanced-proposal-predefined-filter": "tsx rental-model/advanced/proposal-predefined-filter.ts", 19 | "advanced-scan": "tsx core-api/scan.ts", 20 | "advanced-setup-and-teardown": "tsx rental-model/advanced/setup-and-teardown.ts", 21 | "tcp-proxy": "tsx rental-model/advanced/tcp-proxy/tcp-proxy.ts", 22 | "local-image": "tsx rental-model/advanced/local-image/local-image.ts", 23 | "deployment": "tsx experimental/deployment/new-api.ts", 24 | "preweb": "cp -r ../dist/ web/dist/", 25 | "postweb": "rm -rf web/dist/", 26 | "web": "serve web/", 27 | "experimental-server": "tsx experimental/express/server.ts", 28 | "lint": "npm run lint:ts", 29 | "lint:ts": "tsc --project tsconfig.json --noEmit" 30 | }, 31 | "author": "GolemFactory ", 32 | "license": "LGPL-3.0", 33 | "engines": { 34 | "node": ">=18.0.0" 35 | }, 36 | "dependencies": { 37 | "@golem-sdk/golem-js": "file:..", 38 | "@golem-sdk/pino-logger": "^1.1.0", 39 | "@types/express": "^4.17.21", 40 | "commander": "^12.0.0", 41 | "express": "^4.21.1", 42 | "tsx": "^4.19.1", 43 | "viem": "^2.21.1" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "20", 47 | "cypress": "^13.15.0", 48 | "serve": "^14.2.3", 49 | "typescript": "^5.4.5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/deposit/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | funder: { 3 | address: "0x00", 4 | privateKey: "0x00", 5 | nonceSpace: 1000000, 6 | }, 7 | rpcUrl: "https://holesky.rpc-node.dev.golem.network", 8 | lockPaymentContract: { 9 | holeskyAddress: "0x63704675f72A47a7a183112700Cb48d4B0A94332", 10 | }, 11 | glmContract: { 12 | holeskyAddress: "0x8888888815bf4DB87e57B609A50f938311EEd068", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/gpu/bandwidthTest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemfactory/golem-js/76e1342c318b1b62ae0a68fd9b3330999003d4a3/examples/rental-model/advanced/gpu/bandwidthTest -------------------------------------------------------------------------------- /examples/rental-model/advanced/gpu/gpu.ts: -------------------------------------------------------------------------------- 1 | import { GolemNetwork, MarketOrderSpec } from "@golem-sdk/golem-js"; 2 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 3 | 4 | (async () => { 5 | const glm = new GolemNetwork({ 6 | logger: pinoPrettyLogger({ 7 | level: "info", 8 | }), 9 | payment: { 10 | network: "polygon", 11 | }, 12 | }); 13 | 14 | const order: MarketOrderSpec = { 15 | demand: { 16 | workload: { 17 | imageTag: "nvidia/cuda:12.6.0-cudnn-runtime-ubuntu24.04", 18 | capabilities: ["!exp:gpu"], 19 | runtime: { 20 | name: "vm-nvidia", 21 | }, 22 | }, 23 | }, 24 | market: { 25 | rentHours: 0.5, 26 | pricing: { 27 | model: "linear", 28 | maxStartPrice: 0.0, 29 | maxCpuPerHourPrice: 0.0, 30 | maxEnvPerHourPrice: 2.0, 31 | }, 32 | }, 33 | }; 34 | 35 | try { 36 | await glm.connect(); 37 | const rental = await glm.oneOf({ order }); 38 | const exe = await rental.getExeUnit(); 39 | 40 | // The executable binary from the Samples for CUDA Developers package. 41 | // This is a simple test program to measure the memcopy bandwidth of the GPU. 42 | // https://github.com/NVIDIA/cuda-samples 43 | await exe.uploadFile("./bandwidthTest", "/storage/bandwidthTest"); 44 | await exe.run("chmod +x /storage/bandwidthTest"); 45 | 46 | const bandwidthResult = await exe.run("/storage/bandwidthTest"); 47 | console.log("\nCUDA Bandwidth Test:\n\n", bandwidthResult.stdout); 48 | 49 | // Run native command nvidia-smi provided by nvidia driver. 50 | // https://developer.nvidia.com/system-management-interface 51 | const nvidiaSmiResult = await exe.run("nvidia-smi"); 52 | console.log("\n\nNVIDIA SMI Test:\n\n", nvidiaSmiResult.stdout); 53 | 54 | await rental.stopAndFinalize(); 55 | } catch (err) { 56 | console.error("Failed to run the example", err); 57 | } finally { 58 | await glm.disconnect(); 59 | } 60 | })().catch(console.error); 61 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/local-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | WORKDIR /golem/work 3 | RUN echo "hello from my local image 👋" > /golem/work/hello.txt -------------------------------------------------------------------------------- /examples/rental-model/advanced/local-image/alpine.gvmi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemfactory/golem-js/76e1342c318b1b62ae0a68fd9b3330999003d4a3/examples/rental-model/advanced/local-image/alpine.gvmi -------------------------------------------------------------------------------- /examples/rental-model/advanced/local-image/local-image.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates how to upload a local GVMI file to the provider. 3 | * Take a look at the `Dockerfile` in the same directory to see what's inside the image. 4 | */ 5 | import { GolemNetwork, MarketOrderSpec } from "@golem-sdk/golem-js"; 6 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 7 | 8 | // get the absolute path to the local image in case this file is run from a different directory 9 | const getImagePath = (path: string) => new URL(path, import.meta.url).toString(); 10 | 11 | (async () => { 12 | const logger = pinoPrettyLogger({ 13 | level: "info", 14 | }); 15 | 16 | const glm = new GolemNetwork({ 17 | logger, 18 | }); 19 | 20 | try { 21 | await glm.connect(); 22 | 23 | const order: MarketOrderSpec = { 24 | demand: { 25 | workload: { 26 | // if the image url starts with "file://" it will be treated as a local file 27 | // and the sdk will automatically serve it to the provider 28 | imageUrl: getImagePath("./alpine.gvmi"), 29 | }, 30 | }, 31 | market: { 32 | rentHours: 15 / 60, 33 | pricing: { 34 | model: "linear", 35 | maxStartPrice: 1, 36 | maxCpuPerHourPrice: 1, 37 | maxEnvPerHourPrice: 1, 38 | }, 39 | }, 40 | }; 41 | 42 | const rental = await glm.oneOf({ order }); 43 | // in our Dockerfile we have created a file called hello.txt, let's read it 44 | const result = await rental 45 | .getExeUnit() 46 | .then((exe) => exe.run("cat hello.txt")) 47 | .then((res) => res.stdout); 48 | console.log(result); 49 | } catch (err) { 50 | console.error("Failed to run example on Golem", err); 51 | } finally { 52 | await glm.disconnect(); 53 | } 54 | })().catch(console.error); 55 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/outbound/whitelist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "createdAt": "2024-08-21T21:55:37.123+02:00", 4 | "expiresAt": "2024-11-19T21:55:37.123+01:00", 5 | "metadata": { 6 | "name": "outbound-example-project", 7 | "version": "1.0.0" 8 | }, 9 | "payload": [ 10 | { 11 | "platform": { 12 | "os": "linux", 13 | "arch": "x86_64" 14 | }, 15 | "hash": "sha3:f985714e913cf2f448c8dac86b3b4a82d3f2e7ece490e24428d6c675", 16 | "urls": [ 17 | "http://registry.golem.network/download/656b365b59fb63da42918f861a1ddba85c61223021efd4cc9ef0c017089ac0df" 18 | ] 19 | } 20 | ], 21 | "compManifest": { 22 | "version": "0.1.0", 23 | "net": { 24 | "inet": { 25 | "out": { 26 | "urls": ["https://registry.npmjs.org"], 27 | "protocols": ["https"] 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/payment-filters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates how to use payment filters to prevent auto-accepting 3 | * invoices and debit notes that don't meet certain criteria. 4 | */ 5 | import { MarketOrderSpec, GolemNetwork, InvoiceFilter, DebitNoteFilter } from "@golem-sdk/golem-js"; 6 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 7 | 8 | /* let's create a sample filter that doesn't accept invoices if 9 | * the payable amount is higher than 1000 GLM. 10 | * Be careful when processing floating point numbers in JavaScript, as they can 11 | * be imprecise. For this reason, we recommend using a library like decimal.js-light. 12 | * `invoice.getPreciseAmount()` is a method that returns the amount as a Decimal object. 13 | */ 14 | const invoiceFilter: InvoiceFilter = async (invoice) => { 15 | console.debug( 16 | "Invoice %s for %s GLM is passing through the filter", 17 | invoice.id, 18 | invoice.getPreciseAmount().toFixed(6), 19 | ); 20 | return invoice.getPreciseAmount().lte(1000); 21 | }; 22 | 23 | /* Let's create another sample filter. This time we will get the demand that 24 | * the debit note is related to from the provided context and compare the payment platforms. 25 | */ 26 | const debitNoteFilter: DebitNoteFilter = async (debitNote, context) => { 27 | console.debug( 28 | "Debit Note %s for %s GLM is passing through the filter", 29 | debitNote.id, 30 | debitNote.getPreciseAmount().toFixed(6), 31 | ); 32 | return debitNote.paymentPlatform === context.demand.paymentPlatform; 33 | }; 34 | 35 | const order: MarketOrderSpec = { 36 | demand: { 37 | workload: { imageTag: "golem/alpine:latest" }, 38 | }, 39 | market: { 40 | rentHours: 0.5, 41 | pricing: { 42 | model: "linear", 43 | maxStartPrice: 0.5, 44 | maxCpuPerHourPrice: 1.0, 45 | maxEnvPerHourPrice: 0.5, 46 | }, 47 | }, 48 | // Here's where we specify the payment filters 49 | payment: { 50 | debitNoteFilter, 51 | invoiceFilter, 52 | }, 53 | }; 54 | 55 | (async () => { 56 | const glm = new GolemNetwork({ 57 | logger: pinoPrettyLogger({ 58 | level: "info", 59 | }), 60 | }); 61 | 62 | try { 63 | await glm.connect(); 64 | const rental = await glm.oneOf({ order }); 65 | await rental 66 | .getExeUnit() 67 | .then((exe) => exe.run("echo Hello, Golem! 👋")) 68 | .then((res) => console.log(res.stdout)); 69 | await rental.stopAndFinalize(); 70 | } catch (err) { 71 | console.error("Failed to run the example", err); 72 | } finally { 73 | await glm.disconnect(); 74 | } 75 | })().catch(console.error); 76 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/proposal-filter.ts: -------------------------------------------------------------------------------- 1 | import { MarketOrderSpec, GolemNetwork, OfferProposalFilter } from "@golem-sdk/golem-js"; 2 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 3 | 4 | /** 5 | * Example demonstrating how to write a custom offer proposal filter. 6 | * In this case the offer proposal must include VPN access and must not be from "bad-provider" 7 | */ 8 | const myFilter: OfferProposalFilter = (proposal) => 9 | Boolean( 10 | proposal.provider.name !== "bad-provider" && proposal.properties["golem.runtime.capabilities"]?.includes("vpn"), 11 | ); 12 | 13 | const order: MarketOrderSpec = { 14 | demand: { 15 | workload: { imageTag: "golem/alpine:latest" }, 16 | }, 17 | market: { 18 | rentHours: 0.5, 19 | pricing: { 20 | model: "linear", 21 | maxStartPrice: 0.5, 22 | maxCpuPerHourPrice: 1.0, 23 | maxEnvPerHourPrice: 0.5, 24 | }, 25 | offerProposalFilter: myFilter, 26 | }, 27 | }; 28 | 29 | (async () => { 30 | const glm = new GolemNetwork({ 31 | logger: pinoPrettyLogger({ 32 | level: "info", 33 | }), 34 | }); 35 | 36 | try { 37 | await glm.connect(); 38 | const rental = await glm.oneOf({ order }); 39 | await rental 40 | .getExeUnit() 41 | .then((exe) => exe.run(`echo [provider:${exe.provider.name}] Hello, Golem! 👋`)) 42 | .then((res) => console.log(res.stdout)); 43 | await rental.stopAndFinalize(); 44 | } catch (err) { 45 | console.error("Failed to run the example", err); 46 | } finally { 47 | await glm.disconnect(); 48 | } 49 | })().catch(console.error); 50 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/proposal-predefined-filter.ts: -------------------------------------------------------------------------------- 1 | import { MarketOrderSpec, GolemNetwork, OfferProposalFilterFactory } from "@golem-sdk/golem-js"; 2 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 3 | 4 | /** 5 | * Example showing how to use a offer proposal filter using the predefined filter `disallowProvidersByName`, 6 | * which blocks any proposal from a provider whose name is in the array of 7 | */ 8 | 9 | const blackListProvidersNames = ["provider-1", "bad-provider", "slow-provider"]; 10 | 11 | const order: MarketOrderSpec = { 12 | demand: { 13 | workload: { imageTag: "golem/alpine:latest" }, 14 | }, 15 | market: { 16 | rentHours: 0.5, 17 | pricing: { 18 | model: "linear", 19 | maxStartPrice: 0.5, 20 | maxCpuPerHourPrice: 1.0, 21 | maxEnvPerHourPrice: 0.5, 22 | }, 23 | offerProposalFilter: OfferProposalFilterFactory.disallowProvidersByName(blackListProvidersNames), 24 | }, 25 | }; 26 | 27 | (async () => { 28 | const glm = new GolemNetwork({ 29 | logger: pinoPrettyLogger({ 30 | level: "info", 31 | }), 32 | }); 33 | 34 | try { 35 | await glm.connect(); 36 | const rental = await glm.oneOf({ order }); 37 | await rental 38 | .getExeUnit() 39 | .then((exe) => exe.run(`echo [provider:${exe.provider.name}] Hello, Golem! 👋`)) 40 | .then((res) => console.log(res.stdout)); 41 | await rental.stopAndFinalize(); 42 | } catch (err) { 43 | console.error("Failed to run the example", err); 44 | } finally { 45 | await glm.disconnect(); 46 | } 47 | })().catch(console.error); 48 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/proposal-selector.ts: -------------------------------------------------------------------------------- 1 | import { MarketOrderSpec, GolemNetwork, OfferProposal } from "@golem-sdk/golem-js"; 2 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 3 | 4 | /** 5 | * Example demonstrating how to write a selector which choose the best provider based on scores provided as object: [providerName]: score 6 | * A higher score rewards the provider. 7 | */ 8 | const scores = { 9 | "provider-1": 100, 10 | "golem-provider": 50, 11 | "super-provider": 25, 12 | }; 13 | 14 | const bestProviderSelector = (scores: { [providerName: string]: number }) => (proposals: OfferProposal[]) => { 15 | proposals.sort((a, b) => ((scores?.[a.provider.name] || 0) >= (scores?.[b.provider.name] || 0) ? -1 : 1)); 16 | return proposals[0]; 17 | }; 18 | 19 | const order: MarketOrderSpec = { 20 | demand: { 21 | workload: { imageTag: "golem/alpine:latest" }, 22 | }, 23 | market: { 24 | rentHours: 0.5, 25 | pricing: { 26 | model: "linear", 27 | maxStartPrice: 0.5, 28 | maxCpuPerHourPrice: 1.0, 29 | maxEnvPerHourPrice: 0.5, 30 | }, 31 | offerProposalSelector: bestProviderSelector(scores), 32 | }, 33 | }; 34 | 35 | (async () => { 36 | const glm = new GolemNetwork({ 37 | logger: pinoPrettyLogger({ 38 | level: "info", 39 | }), 40 | }); 41 | 42 | try { 43 | await glm.connect(); 44 | const rental = await glm.oneOf({ order }); 45 | await rental 46 | .getExeUnit() 47 | .then((exe) => exe.run(`echo [provider:${exe.provider.name}] Hello, Golem! 👋`)) 48 | .then((res) => console.log(res.stdout)); 49 | await rental.stopAndFinalize(); 50 | } catch (err) { 51 | console.error("Failed to run the example", err); 52 | } finally { 53 | await glm.disconnect(); 54 | } 55 | })().catch(console.error); 56 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/reuse-allocation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This advanced example demonstrates create an allocation manually and then reuse 3 | * it across multiple market orders. 4 | */ 5 | import { MarketOrderSpec, GolemNetwork } from "@golem-sdk/golem-js"; 6 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 7 | 8 | (async () => { 9 | const ALLOCATION_DURATION_HOURS = 1; 10 | const RENTAL_DURATION_HOURS = 0.5; 11 | 12 | console.assert( 13 | ALLOCATION_DURATION_HOURS > RENTAL_DURATION_HOURS, 14 | "Always create allocations that will live longer than the planned rental duration", 15 | ); 16 | 17 | const glm = new GolemNetwork({ 18 | logger: pinoPrettyLogger({ 19 | level: "info", 20 | }), 21 | }); 22 | 23 | try { 24 | await glm.connect(); 25 | 26 | const allocation = await glm.payment.createAllocation({ 27 | budget: 1, 28 | expirationSec: ALLOCATION_DURATION_HOURS * 60 * 60, 29 | }); 30 | 31 | const firstOrder: MarketOrderSpec = { 32 | demand: { 33 | workload: { imageTag: "golem/alpine:latest" }, 34 | }, 35 | market: { 36 | rentHours: RENTAL_DURATION_HOURS, 37 | pricing: { 38 | model: "burn-rate", 39 | avgGlmPerHour: 0.5, 40 | }, 41 | }, 42 | payment: { 43 | // You can either pass the allocation object ... 44 | allocation, 45 | }, 46 | }; 47 | const secondOrder: MarketOrderSpec = { 48 | demand: { 49 | workload: { imageTag: "golem/alpine:latest" }, 50 | }, 51 | market: { 52 | rentHours: RENTAL_DURATION_HOURS, 53 | pricing: { 54 | model: "burn-rate", 55 | avgGlmPerHour: 0.5, 56 | }, 57 | }, 58 | payment: { 59 | // ... or just the allocation ID 60 | allocation: allocation.id, 61 | }, 62 | }; 63 | 64 | const rental1 = await glm.oneOf({ order: firstOrder }); 65 | const rental2 = await glm.oneOf({ order: secondOrder }); 66 | 67 | await rental1 68 | .getExeUnit() 69 | .then((exe) => exe.run("echo Running on first rental")) 70 | .then((res) => console.log(res.stdout)); 71 | await rental2 72 | .getExeUnit() 73 | .then((exe) => exe.run("echo Running on second rental")) 74 | .then((res) => console.log(res.stdout)); 75 | 76 | await rental1.stopAndFinalize(); 77 | await rental2.stopAndFinalize(); 78 | await glm.payment.releaseAllocation(allocation); 79 | } catch (err) { 80 | console.error("Failed to run the example", err); 81 | } finally { 82 | await glm.disconnect(); 83 | } 84 | })().catch(console.error); 85 | -------------------------------------------------------------------------------- /examples/rental-model/advanced/tcp-proxy/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const http = require("http"); 3 | 4 | (async function main() { 5 | const PORT = parseInt(process.env["PORT"] ?? "80"); 6 | 7 | // Increase the value if you want to test long response/liveliness scenarios 8 | const SIMULATE_DELAY_SEC = parseInt(process.env["SIMULATE_DELAY_SEC"] ?? "0"); 9 | 10 | const respond = (res) => { 11 | res.writeHead(200); 12 | res.end("Hello Golem!"); 13 | }; 14 | 15 | const app = http.createServer((req, res) => { 16 | if (SIMULATE_DELAY_SEC > 0) { 17 | setTimeout(() => { 18 | respond(res); 19 | }, SIMULATE_DELAY_SEC * 1000); 20 | } else { 21 | respond(res); 22 | } 23 | }); 24 | 25 | const server = app.listen(PORT, () => console.log(`HTTP server started at "http://localhost:${PORT}"`)); 26 | 27 | const shutdown = () => { 28 | server.close((err) => { 29 | if (err) { 30 | console.error("Server close encountered an issue", err); 31 | } else { 32 | console.log("Server closed successfully"); 33 | } 34 | }); 35 | }; 36 | 37 | process.on("SIGINT", shutdown); 38 | process.on("SIGTERM", shutdown); 39 | })(); 40 | -------------------------------------------------------------------------------- /examples/rental-model/basic/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This example showcases how users can listen to various events exposed from golem-js 3 | */ 4 | import { GolemNetwork } from "@golem-sdk/golem-js"; 5 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 6 | 7 | (async () => { 8 | const glm = new GolemNetwork({ 9 | logger: pinoPrettyLogger({ 10 | level: "info", 11 | }), 12 | payment: { 13 | driver: "erc20", 14 | network: "holesky", 15 | }, 16 | }); 17 | 18 | try { 19 | await glm.connect(); 20 | 21 | glm.market.events.on("agreementApproved", (event) => { 22 | console.log("Agreement '%s' approved", event.agreement.id); 23 | }); 24 | 25 | glm.market.events.on("agreementTerminated", (event) => { 26 | console.log( 27 | "Agreement '%s' terminated by '%s' with reason '%s'", 28 | event.agreement.id, 29 | event.terminatedBy, 30 | event.reason, 31 | ); 32 | }); 33 | 34 | glm.market.events.on("offerCounterProposalRejected", (event) => { 35 | console.warn("Proposal rejected by provider", event); 36 | }); 37 | 38 | const rental = await glm.oneOf({ 39 | order: { 40 | demand: { 41 | workload: { imageTag: "golem/alpine:latest" }, 42 | }, 43 | market: { 44 | rentHours: 0.5, 45 | pricing: { 46 | model: "linear", 47 | maxStartPrice: 0.5, 48 | maxCpuPerHourPrice: 1.0, 49 | maxEnvPerHourPrice: 0.5, 50 | }, 51 | }, 52 | }, 53 | }); 54 | 55 | await rental 56 | .getExeUnit() 57 | .then((exe) => exe.run("echo Hello, Golem! 👋")) 58 | .then((res) => console.log(res.stdout)); 59 | 60 | await rental.stopAndFinalize(); 61 | } catch (err) { 62 | console.error("Failed to run the example", err); 63 | } finally { 64 | await glm.disconnect(); 65 | } 66 | })().catch(console.error); 67 | -------------------------------------------------------------------------------- /examples/rental-model/basic/many-of.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates how easily rent multiple machines at once. 3 | */ 4 | 5 | import { GolemNetwork, MarketOrderSpec } from "@golem-sdk/golem-js"; 6 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 7 | 8 | const order: MarketOrderSpec = { 9 | demand: { 10 | workload: { imageTag: "golem/alpine:latest" }, 11 | }, 12 | market: { 13 | rentHours: 0.5, 14 | pricing: { 15 | model: "linear", 16 | maxStartPrice: 0.5, 17 | maxCpuPerHourPrice: 1.0, 18 | maxEnvPerHourPrice: 0.5, 19 | }, 20 | }, 21 | }; 22 | 23 | (async () => { 24 | const glm = new GolemNetwork({ 25 | logger: pinoPrettyLogger({ 26 | level: "info", 27 | }), 28 | }); 29 | 30 | try { 31 | await glm.connect(); 32 | // create a pool that can grow up to 3 rentals at the same time 33 | const pool = await glm.manyOf({ 34 | poolSize: 3, 35 | order, 36 | }); 37 | await Promise.allSettled([ 38 | pool.withRental(async (rental) => 39 | rental 40 | .getExeUnit() 41 | .then((exe) => exe.run("echo Hello, Golem from the first machine! 👋")) 42 | .then((res) => console.log(res.stdout)), 43 | ), 44 | pool.withRental(async (rental) => 45 | rental 46 | .getExeUnit() 47 | .then((exe) => exe.run("echo Hello, Golem from the second machine! 👋")) 48 | .then((res) => console.log(res.stdout)), 49 | ), 50 | pool.withRental(async (rental) => 51 | rental 52 | .getExeUnit() 53 | .then((exe) => exe.run("echo Hello, Golem from the third machine! 👋")) 54 | .then((res) => console.log(res.stdout)), 55 | ), 56 | ]); 57 | } catch (err) { 58 | console.error("Failed to run the example", err); 59 | } finally { 60 | await glm.disconnect(); 61 | } 62 | })().catch(console.error); 63 | -------------------------------------------------------------------------------- /examples/rental-model/basic/one-of.ts: -------------------------------------------------------------------------------- 1 | import { MarketOrderSpec, GolemNetwork } from "@golem-sdk/golem-js"; 2 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 3 | 4 | const order: MarketOrderSpec = { 5 | demand: { 6 | workload: { imageTag: "golem/alpine:latest" }, 7 | }, 8 | market: { 9 | rentHours: 0.5, 10 | pricing: { 11 | model: "linear", 12 | maxStartPrice: 0.5, 13 | maxCpuPerHourPrice: 1.0, 14 | maxEnvPerHourPrice: 0.5, 15 | }, 16 | }, 17 | }; 18 | 19 | (async () => { 20 | const glm = new GolemNetwork({ 21 | logger: pinoPrettyLogger({ 22 | level: "info", 23 | }), 24 | }); 25 | 26 | try { 27 | await glm.connect(); 28 | const rental = await glm.oneOf({ order }); 29 | await rental 30 | .getExeUnit() 31 | .then((exe) => exe.run("echo Hello, Golem! 👋")) 32 | .then((res) => console.log(res.stdout)); 33 | await rental.stopAndFinalize(); 34 | } catch (err) { 35 | console.error("Failed to run the example", err); 36 | } finally { 37 | await glm.disconnect(); 38 | } 39 | })().catch(console.error); 40 | -------------------------------------------------------------------------------- /examples/rental-model/basic/run-and-stream.ts: -------------------------------------------------------------------------------- 1 | import { MarketOrderSpec, GolemNetwork } from "@golem-sdk/golem-js"; 2 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 3 | 4 | /** 5 | * Example demonstrating the execution of a command on a provider which may take a long time 6 | * and returns the results from the execution during the command as a stream 7 | */ 8 | 9 | const order: MarketOrderSpec = { 10 | demand: { 11 | workload: { imageTag: "golem/alpine:latest" }, 12 | }, 13 | market: { 14 | rentHours: 0.5, 15 | pricing: { 16 | model: "linear", 17 | maxStartPrice: 0.5, 18 | maxCpuPerHourPrice: 1.0, 19 | maxEnvPerHourPrice: 0.5, 20 | }, 21 | }, 22 | }; 23 | 24 | (async () => { 25 | const glm = new GolemNetwork({ 26 | logger: pinoPrettyLogger({ 27 | level: "info", 28 | }), 29 | }); 30 | 31 | try { 32 | await glm.connect(); 33 | const rental = await glm.oneOf({ order }); 34 | const exe = await rental.getExeUnit(); 35 | 36 | const remoteProcess = await exe.runAndStream( 37 | ` 38 | sleep 1 39 | echo -n 'Hello from stdout' >&1 40 | echo -n 'Hello from stderr' >&2 41 | sleep 1 42 | echo -n 'Hello from stdout again' >&1 43 | echo -n 'Hello from stderr again' >&2 44 | sleep 1 45 | echo -n 'Hello from stdout yet again' >&1 46 | echo -n 'Hello from stderr yet again' >&2 47 | `, 48 | ); 49 | remoteProcess.stdout.subscribe((data) => console.log("stdout>", data)); 50 | remoteProcess.stderr.subscribe((data) => console.error("stderr>", data)); 51 | await remoteProcess.waitForExit(); 52 | 53 | await rental.stopAndFinalize(); 54 | } catch (err) { 55 | console.error("Failed to run the example", err); 56 | } finally { 57 | await glm.disconnect(); 58 | } 59 | })().catch(console.error); 60 | -------------------------------------------------------------------------------- /examples/rental-model/basic/transfer.ts: -------------------------------------------------------------------------------- 1 | import { MarketOrderSpec, GolemNetwork } from "@golem-sdk/golem-js"; 2 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 3 | import { appendFile, readFile, unlink } from "fs/promises"; 4 | 5 | const order: MarketOrderSpec = { 6 | demand: { 7 | workload: { imageTag: "golem/alpine:latest" }, 8 | }, 9 | market: { 10 | rentHours: 0.5, 11 | pricing: { 12 | model: "linear", 13 | maxStartPrice: 0.5, 14 | maxCpuPerHourPrice: 1.0, 15 | maxEnvPerHourPrice: 0.5, 16 | }, 17 | }, 18 | }; 19 | 20 | (async () => { 21 | const glm = new GolemNetwork({ 22 | logger: pinoPrettyLogger({ 23 | level: "info", 24 | }), 25 | }); 26 | 27 | try { 28 | await glm.connect(); 29 | const pool = await glm.manyOf({ 30 | poolSize: 2, 31 | order, 32 | }); 33 | const rental1 = await pool.acquire(); 34 | const rental2 = await pool.acquire(); 35 | 36 | const exe1 = await rental1.getExeUnit(); 37 | const exe2 = await rental2.getExeUnit(); 38 | 39 | await exe1 40 | .beginBatch() 41 | .run(`echo "Message from provider ${exe1.provider.name}. Hello 😻" >> /golem/work/message.txt`) 42 | .downloadFile("/golem/work/message.txt", "./message.txt") 43 | .end(); 44 | 45 | await appendFile("./message.txt", "Message from requestor. Hello 🤠\n"); 46 | 47 | await exe2 48 | .beginBatch() 49 | .uploadFile("./message.txt", "/golem/work/message.txt") 50 | .run(`echo "Message from provider ${exe2.provider.name}. Hello 👻" >> /golem/work/message.txt`) 51 | .downloadFile("/golem/work/message.txt", "./results.txt") 52 | .end(); 53 | 54 | console.log("File content: "); 55 | console.log(await readFile("./results.txt", { encoding: "utf-8" })); 56 | 57 | await rental1.stopAndFinalize(); 58 | await rental2.stopAndFinalize(); 59 | } catch (err) { 60 | console.error("Failed to run the example", err); 61 | } finally { 62 | await glm.disconnect(); 63 | await unlink("./message.txt"); 64 | await unlink("./results.txt"); 65 | } 66 | })().catch(console.error); 67 | -------------------------------------------------------------------------------- /examples/rental-model/basic/vpn.ts: -------------------------------------------------------------------------------- 1 | import { MarketOrderSpec, GolemNetwork } from "@golem-sdk/golem-js"; 2 | import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; 3 | 4 | (async () => { 5 | const glm = new GolemNetwork({ 6 | logger: pinoPrettyLogger({ 7 | level: "info", 8 | }), 9 | }); 10 | 11 | try { 12 | await glm.connect(); 13 | const network = await glm.createNetwork({ ip: "192.168.7.0/24" }); 14 | const order: MarketOrderSpec = { 15 | demand: { 16 | workload: { 17 | imageTag: "golem/alpine:latest", 18 | capabilities: ["vpn"], 19 | }, 20 | }, 21 | market: { 22 | rentHours: 0.5, 23 | pricing: { 24 | model: "linear", 25 | maxStartPrice: 0.5, 26 | maxCpuPerHourPrice: 1.0, 27 | maxEnvPerHourPrice: 0.5, 28 | }, 29 | }, 30 | network, 31 | }; 32 | // create a pool that can grow up to 2 rentals at the same time 33 | const pool = await glm.manyOf({ 34 | poolSize: 2, 35 | order, 36 | }); 37 | const rental1 = await pool.acquire(); 38 | const rental2 = await pool.acquire(); 39 | const exe1 = await rental1.getExeUnit(); 40 | const exe2 = await rental2.getExeUnit(); 41 | await exe1 42 | .run(`ping ${exe2.getIp()} -c 4`) 43 | .then((res) => console.log(`Response from provider: ${exe1.provider.name} (ip: ${exe1.getIp()})`, res.stdout)); 44 | await exe2 45 | .run(`ping ${exe1.getIp()} -c 4`) 46 | .then((res) => console.log(`Response from provider: ${exe2.provider.name} (ip: ${exe2.getIp()})`, res.stdout)); 47 | await pool.destroy(rental1); 48 | await pool.destroy(rental2); 49 | 50 | await glm.destroyNetwork(network); 51 | } catch (err) { 52 | console.error("Failed to run the example", err); 53 | } finally { 54 | await glm.disconnect(); 55 | } 56 | })().catch(console.error); 57 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "strict": true, 6 | "noImplicitAny": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "Bundler", 9 | "removeComments": true, 10 | "sourceMap": true, 11 | "noLib": false, 12 | "skipLibCheck": true, 13 | "declaration": true, 14 | "lib": ["es2015", "es2016", "es2017", "es2018", "esnext", "dom"], 15 | "outDir": "dist", 16 | "typeRoots": ["node_modules/@types"] 17 | }, 18 | "exclude": ["dist", "node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "setupFilesAfterEnv": ["./tests/jest.setup.ts"], 5 | "transform": { 6 | "^.+\\.tsx?$": [ 7 | "ts-jest", 8 | { 9 | "tsconfig": "tsconfig.spec.json" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import terser from "@rollup/plugin-terser"; 4 | import typescript from "@rollup/plugin-typescript"; 5 | import nodePolyfills from "rollup-plugin-polyfill-node"; 6 | import pkg from "./package.json" with { type: "json" }; 7 | import ignore from "rollup-plugin-ignore"; 8 | import filesize from "rollup-plugin-filesize"; 9 | import fs from "node:fs"; 10 | import { fileURLToPath } from "node:url"; 11 | 12 | /** 13 | * Looking for plugins? 14 | * 15 | * Check: {@link https://github.com/rollup/awesome} 16 | */ 17 | 18 | export default [ 19 | // Browser 20 | { 21 | input: "src/index.ts", 22 | output: { 23 | inlineDynamicImports: true, 24 | file: pkg.browser, 25 | name: "GolemJs", 26 | sourcemap: true, 27 | format: "es", 28 | }, 29 | plugins: [ 30 | deleteExistingBundles("dist"), 31 | ignore(["tmp"]), 32 | nodeResolve({ browser: true, preferBuiltins: true }), 33 | commonjs(), 34 | nodePolyfills(), 35 | typescript({ tsconfig: "./tsconfig.json", exclude: ["**/*.test.ts"] }), 36 | terser(), 37 | filesize({ reporter: [sizeValidator, "boxen"] }), 38 | ], 39 | }, 40 | // NodeJS 41 | { 42 | input: { 43 | "golem-js": "src/index.ts", 44 | "golem-js-experimental": "src/experimental/index.ts", 45 | }, 46 | output: { 47 | dir: "dist", 48 | format: "esm", 49 | sourcemap: true, 50 | chunkFileNames: "shared-[hash].mjs", 51 | entryFileNames: "[name].mjs", 52 | }, 53 | plugins: [ 54 | typescript({ tsconfig: "./tsconfig.json", exclude: ["**/*.test.ts"] }), 55 | filesize({ reporter: [sizeValidator, "boxen"] }), 56 | ], 57 | }, 58 | { 59 | input: { 60 | "golem-js": "src/index.ts", 61 | "golem-js-experimental": "src/experimental/index.ts", 62 | }, 63 | output: { 64 | dir: "dist", 65 | format: "cjs", 66 | sourcemap: true, 67 | chunkFileNames: "shared-[hash].js", 68 | }, 69 | plugins: [ 70 | typescript({ 71 | tsconfig: "./tsconfig.json", 72 | exclude: ["**/*.test.ts"], 73 | module: "ES2020", 74 | }), 75 | filesize({ reporter: [sizeValidator, "boxen"] }), 76 | ], 77 | }, 78 | ]; 79 | 80 | function deleteExistingBundles(path) { 81 | return { 82 | name: "delete-existing-bundles", 83 | buildStart: () => { 84 | const distDir = fileURLToPath(new URL(path, import.meta.url).toString()); 85 | if (fs.existsSync(distDir)) { 86 | fs.rmSync(distDir, { recursive: true }); 87 | } 88 | console.log("Deleted " + distDir); 89 | }, 90 | }; 91 | } 92 | 93 | function sizeValidator(options, bundle, { bundleSize }) { 94 | if (parseInt(bundleSize) === 0) { 95 | throw new Error(`Something went wrong while building. Bundle size = ${bundleSize}`); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/activity/activity.test.ts: -------------------------------------------------------------------------------- 1 | import { Activity, ActivityStateEnum, Agreement } from "../index"; 2 | import { instance, mock } from "@johanblumenberg/ts-mockito"; 3 | 4 | const mockAgreement = mock(Agreement); 5 | 6 | describe("Activity", () => { 7 | describe("Getting state", () => { 8 | it("should get activity state", () => { 9 | const activity = new Activity( 10 | "activity-id", 11 | instance(mockAgreement), 12 | ActivityStateEnum.Initialized, 13 | ActivityStateEnum.New, 14 | { 15 | currentUsage: [0.0, 0.0, 0.0], 16 | timestamp: Date.now(), 17 | }, 18 | ); 19 | const state = activity.getState(); 20 | const prev = activity.getPreviousState(); 21 | expect(state).toEqual(ActivityStateEnum.Initialized); 22 | expect(prev).toEqual(ActivityStateEnum.New); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/activity/activity.ts: -------------------------------------------------------------------------------- 1 | import { Agreement, ProviderInfo } from "../market/agreement"; 2 | 3 | export enum ActivityStateEnum { 4 | New = "New", 5 | Initialized = "Initialized", 6 | Deployed = "Deployed", 7 | Ready = "Ready", 8 | Unresponsive = "Unresponsive", 9 | Terminated = "Terminated", 10 | /** In case when we couldn't establish the in on yagna */ 11 | Unknown = "Unknown", 12 | } 13 | 14 | export type ActivityUsageInfo = { 15 | currentUsage?: number[]; 16 | timestamp: number; 17 | }; 18 | 19 | export interface IActivityRepository { 20 | getById(id: string): Promise; 21 | 22 | getStateOfActivity(id: string): Promise; 23 | } 24 | 25 | /** 26 | * Activity module - an object representing the runtime environment on the provider in accordance with the `Package` specification. 27 | * As part of a given activity, it is possible to execute exe script commands and capture their results. 28 | */ 29 | export class Activity { 30 | /** 31 | * @param id The ID of the activity in Yagna 32 | * @param agreement The agreement that's related to this activity 33 | * @param currentState The current state as it was obtained from yagna 34 | * @param previousState The previous state (or New if this is the first time we're creating the activity) 35 | * @param usage Current resource usage vector information 36 | */ 37 | constructor( 38 | public readonly id: string, 39 | public readonly agreement: Agreement, 40 | protected readonly currentState: ActivityStateEnum = ActivityStateEnum.New, 41 | protected readonly previousState: ActivityStateEnum = ActivityStateEnum.Unknown, 42 | protected readonly usage: ActivityUsageInfo, 43 | ) {} 44 | 45 | public get provider(): ProviderInfo { 46 | return this.agreement.provider; 47 | } 48 | 49 | public getState() { 50 | return this.currentState; 51 | } 52 | 53 | public getPreviousState() { 54 | return this.previousState; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/activity/api.ts: -------------------------------------------------------------------------------- 1 | import { Activity, ActivityStateEnum } from "./activity"; 2 | import { Agreement } from "../market"; 3 | import { ExeScriptRequest } from "./exe-script-executor"; 4 | import { Result, StreamingBatchEvent } from "./results"; 5 | import { Observable } from "rxjs"; 6 | 7 | export type ActivityEvents = { 8 | activityCreated: (event: { activity: Activity }) => void; 9 | errorCreatingActivity: (event: { error: Error }) => void; 10 | 11 | activityDestroyed: (event: { activity: Activity }) => void; 12 | errorDestroyingActivity: (event: { activity: Activity; error: Error }) => void; 13 | 14 | exeUnitInitialized: (event: { activity: Activity }) => void; 15 | errorInitializingExeUnit: (event: { activity: Activity; error: Error }) => void; 16 | 17 | activityStateChanged: (event: { activity: Activity; previousState: ActivityStateEnum }) => void; 18 | errorRefreshingActivity: (event: { activity: Activity; error: Error }) => void; 19 | 20 | scriptSent: (event: { activity: Activity; script: ExeScriptRequest }) => void; 21 | scriptExecuted: (event: { activity: Activity; script: ExeScriptRequest; result: string }) => void; 22 | errorExecutingScript: (event: { activity: Activity; script: ExeScriptRequest; error: Error }) => void; 23 | 24 | batchResultsReceived: (event: { activity: Activity; batchId: string; results: Result[] }) => void; 25 | errorGettingBatchResults: (event: { activity: Activity; batchId: string; error: Error }) => void; 26 | 27 | batchEventsReceived: (event: { activity: Activity; batchId: string; event: StreamingBatchEvent }) => void; 28 | errorGettingBatchEvents: (event: { activity: Activity; batchId: string; error: Error }) => void; 29 | }; 30 | 31 | /** 32 | * Represents a set of use cases related to managing the lifetime of an activity 33 | */ 34 | export interface IActivityApi { 35 | getActivity(id: string): Promise; 36 | 37 | createActivity(agreement: Agreement): Promise; 38 | 39 | destroyActivity(activity: Activity): Promise; 40 | 41 | getActivityState(id: string): Promise; 42 | 43 | executeScript(activity: Activity, script: ExeScriptRequest): Promise; 44 | 45 | getExecBatchResults(activity: Activity, batchId: string, commandIndex?: number, timeout?: number): Promise; 46 | 47 | getExecBatchEvents(activity: Activity, batchId: string, commandIndex?: number): Observable; 48 | } 49 | -------------------------------------------------------------------------------- /src/activity/config.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionOptions } from "./exe-script-executor"; 2 | 3 | const DEFAULTS = { 4 | activityExeBatchResultPollIntervalSeconds: 5, 5 | activityExeBatchResultMaxRetries: 20, 6 | }; 7 | 8 | /** 9 | * @internal 10 | */ 11 | export class ExecutionConfig { 12 | public readonly activityExeBatchResultPollIntervalSeconds: number; 13 | public readonly activityExeBatchResultMaxRetries: number; 14 | 15 | constructor(options?: ExecutionOptions) { 16 | this.activityExeBatchResultMaxRetries = 17 | options?.activityExeBatchResultMaxRetries || DEFAULTS.activityExeBatchResultMaxRetries; 18 | this.activityExeBatchResultPollIntervalSeconds = 19 | options?.activityExeBatchResultPollIntervalSeconds || DEFAULTS.activityExeBatchResultPollIntervalSeconds; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/activity/exe-unit/error.ts: -------------------------------------------------------------------------------- 1 | import { GolemModuleError } from "../../shared/error/golem-error"; 2 | import { Agreement, ProviderInfo } from "../../market/agreement"; 3 | import { Activity } from "../index"; 4 | 5 | export enum WorkErrorCode { 6 | ServiceNotInitialized = "ServiceNotInitialized", 7 | ScriptExecutionFailed = "ScriptExecutionFailed", 8 | ActivityDestroyingFailed = "ActivityDestroyingFailed", 9 | ActivityResultsFetchingFailed = "ActivityResultsFetchingFailed", 10 | ActivityCreationFailed = "ActivityCreationFailed", 11 | NetworkSetupMissing = "NetworkSetupMissing", 12 | ScriptInitializationFailed = "ScriptInitializationFailed", 13 | ActivityDeploymentFailed = "ActivityDeploymentFailed", 14 | ActivityStatusQueryFailed = "ActivityStatusQueryFailed", 15 | ActivityResetFailed = "ActivityResetFailed", 16 | } 17 | export class GolemWorkError extends GolemModuleError { 18 | #agreement?: Agreement; 19 | #activity?: Activity; 20 | #provider?: ProviderInfo; 21 | constructor( 22 | message: string, 23 | public code: WorkErrorCode, 24 | agreement?: Agreement, 25 | activity?: Activity, 26 | provider?: ProviderInfo, 27 | public previous?: Error, 28 | ) { 29 | super(message, code, previous); 30 | this.#agreement = agreement; 31 | this.#activity = activity; 32 | this.#provider = provider; 33 | } 34 | public getAgreement(): Agreement | undefined { 35 | return this.#agreement; 36 | } 37 | public getActivity(): Activity | undefined { 38 | return this.#activity; 39 | } 40 | public getProvider(): ProviderInfo | undefined { 41 | return this.#provider; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/activity/exe-unit/index.ts: -------------------------------------------------------------------------------- 1 | export { ExeUnit, LifecycleFunction, ExeUnitOptions } from "./exe-unit"; 2 | export { Batch } from "./batch"; 3 | export { RemoteProcess } from "./process"; 4 | export { GolemWorkError, WorkErrorCode } from "./error"; 5 | export { TcpProxy } from "../../network/tcp-proxy"; 6 | -------------------------------------------------------------------------------- /src/activity/index.ts: -------------------------------------------------------------------------------- 1 | export { Activity, ActivityStateEnum } from "./activity"; 2 | export { Result } from "./results"; 3 | export { ExecutionConfig } from "./config"; 4 | export * from "./activity.module"; 5 | export * from "./exe-unit"; 6 | export * from "./api"; 7 | -------------------------------------------------------------------------------- /src/activity/results.test.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "./results"; 2 | 3 | describe("Results", () => { 4 | describe("converting output to JSON", () => { 5 | describe("positive cases", () => { 6 | test("produces JSON when the stdout contains correct data", () => { 7 | const result = new Result({ 8 | index: 0, 9 | result: "Ok", 10 | stdout: '{ "value": 55 }\n', 11 | stderr: null, 12 | message: null, 13 | isBatchFinished: true, 14 | eventDate: "2023-08-29T09:23:52.305095307Z", 15 | }); 16 | 17 | expect(result.getOutputAsJson()).toEqual({ 18 | value: 55, 19 | }); 20 | }); 21 | }); 22 | 23 | describe("negative cases", () => { 24 | test("throws an error when stdout does not contain nice JSON", () => { 25 | const result = new Result({ 26 | index: 0, 27 | result: "Ok", 28 | stdout: "not json\n", 29 | stderr: null, 30 | message: null, 31 | isBatchFinished: true, 32 | eventDate: "2023-08-29T09:23:52.305095307Z", 33 | }); 34 | 35 | expect(() => result.getOutputAsJson()).toThrow("Failed to parse output to JSON!"); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/activity/script/index.ts: -------------------------------------------------------------------------------- 1 | export { Script } from "./script"; 2 | export { 3 | Command, 4 | Run, 5 | Deploy, 6 | Start, 7 | Transfer, 8 | Terminate, 9 | UploadFile, 10 | UploadData, 11 | DownloadFile, 12 | DownloadData, 13 | Capture, 14 | } from "./command"; 15 | -------------------------------------------------------------------------------- /src/activity/script/script.ts: -------------------------------------------------------------------------------- 1 | import { ActivityApi } from "ya-ts-client"; 2 | import { Command } from "./command"; 3 | import { Result } from "../index"; 4 | import { GolemInternalError } from "../../shared/error/golem-error"; 5 | 6 | /** 7 | * Represents a series of Commands that can be sent to exe-unit via yagna's API 8 | */ 9 | export class Script { 10 | constructor(private commands: Command[] = []) {} 11 | 12 | static create(commands?: Command[]): Script { 13 | return new Script(commands); 14 | } 15 | 16 | add(command: Command) { 17 | this.commands.push(command); 18 | } 19 | 20 | async before() { 21 | await Promise.all(this.commands.map((cmd) => cmd.before())); 22 | } 23 | 24 | async after(results: Result[]): Promise { 25 | // Call after() for each command mapping its result. 26 | return Promise.all(this.commands.map((command, i) => command.after(results[i]))); 27 | } 28 | 29 | getExeScriptRequest(): ActivityApi.ExeScriptRequestDTO { 30 | if (!this.commands.length) { 31 | throw new GolemInternalError("There are no commands in the script"); 32 | } 33 | return { text: JSON.stringify(this.commands.map((cmd) => cmd.toJson())) }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/experimental/README.md: -------------------------------------------------------------------------------- 1 | # Experimental Features 2 | 3 | > **WARNING:** Features present in this directory are experimental and are not yet ready for production use. 4 | > They are subject to change without notice and within minor versions of the library. 5 | -------------------------------------------------------------------------------- /src/experimental/deployment/builder.ts: -------------------------------------------------------------------------------- 1 | import { GolemConfigError } from "../../shared/error/golem-error"; 2 | import { NetworkOptions } from "../../network"; 3 | import { Deployment, DeploymentComponents } from "./deployment"; 4 | import { GolemNetwork, MarketOrderSpec } from "../../golem-network"; 5 | import { validateDeployment } from "./validate-deployment"; 6 | 7 | export interface DeploymentOptions { 8 | replicas: number | { min: number; max: number }; 9 | network?: string; 10 | } 11 | 12 | export interface CreateResourceRentalPoolOptions extends MarketOrderSpec { 13 | deployment: DeploymentOptions; 14 | } 15 | 16 | export class GolemDeploymentBuilder { 17 | private components: DeploymentComponents = { 18 | resourceRentalPools: [], 19 | networks: [], 20 | }; 21 | 22 | public reset() { 23 | this.components = { 24 | resourceRentalPools: [], 25 | networks: [], 26 | }; 27 | } 28 | 29 | constructor(private glm: GolemNetwork) {} 30 | 31 | createResourceRentalPool(name: string, options: CreateResourceRentalPoolOptions): this { 32 | if (this.components.resourceRentalPools.some((pool) => pool.name === name)) { 33 | throw new GolemConfigError(`Resource Rental Pool with name ${name} already exists`); 34 | } 35 | 36 | this.components.resourceRentalPools.push({ name, options }); 37 | 38 | return this; 39 | } 40 | 41 | createNetwork(name: string, options: NetworkOptions = {}): this { 42 | if (this.components.networks.some((network) => network.name === name)) { 43 | throw new GolemConfigError(`Network with name ${name} already exists`); 44 | } 45 | 46 | this.components.networks.push({ name, options }); 47 | 48 | return this; 49 | } 50 | 51 | getDeployment(): Deployment { 52 | validateDeployment(this.components); 53 | const deployment = new Deployment(this.components, { 54 | logger: this.glm.services.logger, 55 | yagna: this.glm.services.yagna, 56 | payment: this.glm.payment, 57 | market: this.glm.market, 58 | activity: this.glm.activity, 59 | network: this.glm.network, 60 | rental: this.glm.rental, 61 | }); 62 | 63 | this.reset(); 64 | 65 | return deployment; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/experimental/deployment/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./deployment"; 2 | export * from "./builder"; 3 | -------------------------------------------------------------------------------- /src/experimental/deployment/validate-deployment.ts: -------------------------------------------------------------------------------- 1 | import { GolemConfigError } from "../../shared/error/golem-error"; 2 | import { DeploymentComponents } from "./deployment"; 3 | 4 | function validateNetworks(components: DeploymentComponents) { 5 | const networkNames = new Set(components.networks.map((network) => network.name)); 6 | for (const pool of components.resourceRentalPools) { 7 | if (!pool.options.deployment?.network) { 8 | continue; 9 | } 10 | if (!networkNames.has(pool.options.deployment.network)) { 11 | throw new GolemConfigError( 12 | `Activity pool ${pool.name} references non-existing network ${pool.options.deployment.network}`, 13 | ); 14 | } 15 | } 16 | } 17 | 18 | export function validateDeployment(components: DeploymentComponents) { 19 | validateNetworks(components); 20 | // ... other validations 21 | } 22 | -------------------------------------------------------------------------------- /src/experimental/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./job"; 2 | export * from "./reputation"; 3 | export * from "./deployment"; 4 | -------------------------------------------------------------------------------- /src/experimental/job/index.ts: -------------------------------------------------------------------------------- 1 | export { Job, JobState } from "./job"; 2 | export { JobManager, JobManagerConfig } from "./job_manager"; 3 | -------------------------------------------------------------------------------- /src/experimental/job/job.test.ts: -------------------------------------------------------------------------------- 1 | import { Job } from "./job"; 2 | import { ExeUnit } from "../../activity/exe-unit"; 3 | import { anything, imock, instance, mock, reset, verify, when } from "@johanblumenberg/ts-mockito"; 4 | import { Logger } from "../../shared/utils"; 5 | import { GolemNetwork } from "../../golem-network"; 6 | import { ResourceRental } from "../../resource-rental"; 7 | 8 | const mockGlm = mock(GolemNetwork); 9 | const mockRental = mock(ResourceRental); 10 | const mockExeUnit = mock(ExeUnit); 11 | describe("Job", () => { 12 | beforeEach(() => { 13 | reset(mockGlm); 14 | reset(mockRental); 15 | reset(mockExeUnit); 16 | }); 17 | 18 | describe("cancel()", () => { 19 | it("stops the activity and releases the agreement when canceled", async () => { 20 | when(mockRental.getExeUnit()).thenResolve(instance(mockExeUnit)); 21 | when(mockGlm.oneOf(anything())).thenResolve(instance(mockRental)); 22 | const job = new Job( 23 | "test_id", 24 | instance(mockGlm), 25 | { 26 | demand: { 27 | workload: { 28 | imageTag: "test_image", 29 | }, 30 | }, 31 | market: { 32 | rentHours: 1, 33 | pricing: { 34 | model: "linear", 35 | maxStartPrice: 1, 36 | maxEnvPerHourPrice: 1, 37 | maxCpuPerHourPrice: 1, 38 | }, 39 | }, 40 | }, 41 | instance(imock()), 42 | ); 43 | 44 | job.startWork(() => { 45 | return new Promise((resolve) => setTimeout(resolve, 10000)); 46 | }); 47 | 48 | await job.cancel(); 49 | 50 | await expect(job.waitForResult()).rejects.toThrow("Canceled"); 51 | 52 | verify(mockRental.stopAndFinalize()).once(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/experimental/job/job_manager.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | import { Job } from "./job"; 3 | import { defaultLogger, Logger, YagnaOptions } from "../../shared/utils"; 4 | import { GolemUserError } from "../../shared/error/golem-error"; 5 | import { GolemNetwork, GolemNetworkOptions, MarketOrderSpec } from "../../golem-network/golem-network"; 6 | 7 | export type JobManagerConfig = Partial & { 8 | /** 9 | * @deprecated This field is deprecated and will be removed in future versions. Please use the 'api.key` and `api.url' instead. 10 | */ 11 | yagna?: YagnaOptions; 12 | }; 13 | 14 | /** 15 | * @experimental This API is experimental and subject to change. Use at your own risk. 16 | * 17 | * The Golem Network class provides a high-level API for running jobs on the Golem Network. 18 | */ 19 | export class JobManager { 20 | private glm: GolemNetwork; 21 | private jobs = new Map(); 22 | 23 | /** 24 | * @param config - Configuration options that will be passed to all jobs created by this instance. 25 | * @param logger 26 | */ 27 | constructor( 28 | config?: JobManagerConfig, 29 | private readonly logger: Logger = defaultLogger("jobs"), 30 | ) { 31 | this.glm = new GolemNetwork({ 32 | api: { 33 | key: config?.yagna?.apiKey, 34 | url: config?.yagna?.basePath, 35 | }, 36 | logger: this.logger, 37 | ...config, 38 | }); 39 | } 40 | 41 | public isInitialized() { 42 | return this.glm.isConnected(); 43 | } 44 | 45 | public async init() { 46 | await this.glm.connect(); 47 | } 48 | 49 | /** 50 | * Create a new job and add it to the list of jobs managed by this instance. 51 | * This method does not start any work on the network, use {@link experimental/job/job.Job.startWork} for that. 52 | * 53 | * @param order 54 | */ 55 | public createJob(order: MarketOrderSpec) { 56 | this.checkInitialization(); 57 | 58 | const jobId = v4(); 59 | const job = new Job(jobId, this.glm, order, this.logger); 60 | this.jobs.set(jobId, job); 61 | 62 | return job; 63 | } 64 | 65 | public getJobById(id: string) { 66 | this.checkInitialization(); 67 | 68 | return this.jobs.get(id); 69 | } 70 | 71 | /** 72 | * Close the connection to the Yagna service and cancel all running jobs. 73 | */ 74 | public async close() { 75 | const pendingJobs = Array.from(this.jobs.values()).filter((job) => job.isRunning()); 76 | await Promise.allSettled(pendingJobs.map((job) => job.cancel())); 77 | await this.glm.disconnect(); 78 | } 79 | 80 | private checkInitialization() { 81 | if (!this.isInitialized()) { 82 | throw new GolemUserError("GolemNetwork not initialized, please run init() first"); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/experimental/reputation/error.ts: -------------------------------------------------------------------------------- 1 | import { GolemModuleError } from "../../shared/error/golem-error"; 2 | 3 | export class GolemReputationError extends GolemModuleError { 4 | constructor(message: string, cause?: Error) { 5 | super(message, 0, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/experimental/reputation/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ReputationSystem, 3 | DEFAULT_AGREEMENT_WEIGHTS, 4 | DEFAULT_PROPOSAL_WEIGHTS, 5 | DEFAULT_PROPOSAL_MIN_SCORE, 6 | DEFAULT_REPUTATION_URL, 7 | DEFAULT_AGREEMENT_TOP_POOL_SIZE, 8 | } from "./system"; 9 | export { ReputationWeights } from "./types"; 10 | export { ProposalSelectorOptions } from "./types"; 11 | export { ProposalFilterOptions } from "./types"; 12 | export { ReputationData } from "./types"; 13 | export { ReputationProviderEntry } from "./types"; 14 | export { ReputationProviderScores } from "./types"; 15 | -------------------------------------------------------------------------------- /src/golem-network/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./golem-network"; 2 | export { GolemPluginInitializer, GolemPluginOptions, GolemPluginDisconnect } from "./plugin"; 3 | -------------------------------------------------------------------------------- /src/golem-network/plugin.ts: -------------------------------------------------------------------------------- 1 | import { GolemNetwork } from "./golem-network"; 2 | 3 | /** 4 | * Represents a generic cleanup task function that will be executed when the plugin lifetime reaches its end 5 | */ 6 | export type GolemPluginDisconnect = () => Promise | void; 7 | 8 | /** 9 | * Generic type for plugin options 10 | * 11 | * Plugin options are optional by design and plugin developers should use this type when they 12 | * want to enforce type safety on their plugin usage 13 | */ 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | export type GolemPluginOptions = Record | undefined; 16 | 17 | /** 18 | * A generic plugin registration/connect function 19 | * 20 | * Golem plugins are initialized during {@link GolemNetwork.connect} 21 | * 22 | * A plugin initializer may return a cleanup function which will be called during {@link GolemNetwork.disconnect} 23 | */ 24 | export type GolemPluginInitializer = ( 25 | glm: GolemNetwork, 26 | options: T, 27 | ) => void | GolemPluginDisconnect | Promise; 28 | 29 | /** 30 | * Internal data structure that allows deferring plugin initialization to the `connect` call 31 | */ 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | export type GolemPluginRegistration = { 34 | /** Plugin initialization function */ 35 | initializer: GolemPluginInitializer; 36 | /** Options to pass to the initialization function when it's executed */ 37 | options?: T; 38 | }; 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // High-level entry points 2 | export * from "./golem-network"; 3 | export * from "./resource-rental"; 4 | 5 | // Low level entry points for advanced users 6 | export * from "./market"; 7 | export * from "./payment"; 8 | export * from "./network"; 9 | export * from "./activity"; 10 | 11 | // Necessary domain entities for users to consume 12 | export * from "./shared/error/golem-error"; 13 | export * from "./network/tcp-proxy"; 14 | 15 | // Internals 16 | export * from "./shared/utils"; 17 | export * from "./shared/yagna"; 18 | export * from "./shared/storage"; 19 | -------------------------------------------------------------------------------- /src/market/agreement/agreement-event.ts: -------------------------------------------------------------------------------- 1 | import { Agreement } from "./agreement"; 2 | 3 | export type AgreementApproved = { 4 | type: "AgreementApproved"; 5 | agreement: Agreement; 6 | timestamp: Date; 7 | }; 8 | 9 | export type AgreementTerminatedEvent = { 10 | type: "AgreementTerminated"; 11 | terminatedBy: "Provider" | "Requestor"; 12 | reason: string; 13 | agreement: Agreement; 14 | timestamp: Date; 15 | }; 16 | 17 | export type AgreementRejectedEvent = { 18 | type: "AgreementRejected"; 19 | agreement: Agreement; 20 | reason: string; 21 | timestamp: Date; 22 | }; 23 | 24 | export type AgreementCancelledEvent = { 25 | type: "AgreementCancelled"; 26 | agreement: Agreement; 27 | timestamp: Date; 28 | }; 29 | 30 | export type AgreementEvent = 31 | | AgreementApproved 32 | | AgreementTerminatedEvent 33 | | AgreementRejectedEvent 34 | | AgreementCancelledEvent; 35 | -------------------------------------------------------------------------------- /src/market/agreement/agreement.test.ts: -------------------------------------------------------------------------------- 1 | import { Agreement, Demand, DemandSpecification } from "../index"; 2 | import { MarketApi } from "ya-ts-client"; 3 | 4 | const agreementData: MarketApi.AgreementDTO = { 5 | agreementId: "agreement-id", 6 | demand: { 7 | demandId: "demand-id", 8 | requestorId: "requestor-id", 9 | properties: {}, 10 | constraints: "", 11 | timestamp: "2024-01-01T00:00:00.000Z", 12 | }, 13 | offer: { 14 | offerId: "offer-id", 15 | providerId: "provider-id", 16 | properties: { 17 | "golem.node.id.name": "provider-name", 18 | "golem.com.payment.platform.erc20-holesky-tglm.address": "0xProviderWallet", 19 | }, 20 | constraints: "", 21 | timestamp: "2024-01-01T00:00:00.000Z", 22 | }, 23 | state: "Approved", 24 | timestamp: "2024-01-01T00:00:00.000Z", 25 | validTo: "2024-01-02T00:00:00.000Z", 26 | }; 27 | 28 | const demand = new Demand( 29 | "demand-id", 30 | new DemandSpecification( 31 | { 32 | constraints: [], 33 | properties: [], 34 | }, 35 | "erc20-holesky-tglm", 36 | ), 37 | ); 38 | 39 | describe("Agreement", () => { 40 | describe("get provider()", () => { 41 | it("should be a instance ProviderInfo with provider details", () => { 42 | const agreement = new Agreement(agreementData.agreementId, agreementData, demand); 43 | expect(agreement.provider.id).toEqual("provider-id"); 44 | expect(agreement.provider.name).toEqual("provider-name"); 45 | expect(agreement.provider.walletAddress).toEqual("0xProviderWallet"); 46 | }); 47 | }); 48 | 49 | describe("getState()", () => { 50 | it("should return state of agreement", () => { 51 | const agreement = new Agreement(agreementData.agreementId, agreementData, demand); 52 | expect(agreement.getState()).toEqual("Approved"); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/market/agreement/agreement.ts: -------------------------------------------------------------------------------- 1 | import { MarketApi } from "ya-ts-client"; 2 | import { Demand } from "../demand"; 3 | 4 | /** 5 | * * `Proposal` - newly created by a Requestor (draft based on Proposal) 6 | * * `Pending` - confirmed by a Requestor and send to Provider for approval 7 | * * `Cancelled` by a Requestor 8 | * * `Rejected` by a Provider 9 | * * `Approved` by both sides 10 | * * `Expired` - not approved, rejected nor cancelled within validity period 11 | * * `Terminated` - finished after approval. 12 | * 13 | */ 14 | export type AgreementState = "Proposal" | "Pending" | "Cancelled" | "Rejected" | "Approved" | "Expired" | "Terminated"; 15 | 16 | export interface ProviderInfo { 17 | name: string; 18 | id: string; 19 | walletAddress: string; 20 | } 21 | 22 | export interface AgreementOptions { 23 | expirationSec?: number; 24 | waitingForApprovalTimeoutSec?: number; 25 | } 26 | 27 | export interface IAgreementRepository { 28 | getById(id: string): Promise; 29 | } 30 | 31 | /** 32 | * Agreement module - an object representing the contract between the requestor and the provider. 33 | */ 34 | export class Agreement { 35 | /** 36 | * @param id 37 | * @param model 38 | * @param demand 39 | */ 40 | constructor( 41 | public readonly id: string, 42 | private readonly model: MarketApi.AgreementDTO, 43 | public readonly demand: Demand, 44 | ) {} 45 | 46 | /** 47 | * Return agreement state 48 | * @return state 49 | */ 50 | getState() { 51 | return this.model.state; 52 | } 53 | 54 | get provider(): ProviderInfo { 55 | return { 56 | id: this.model.offer.providerId, 57 | name: this.model.offer.properties["golem.node.id.name"], 58 | walletAddress: this.model.offer.properties[`golem.com.payment.platform.${this.demand.paymentPlatform}.address`], 59 | }; 60 | } 61 | 62 | /** 63 | * Returns flag if the agreement is in the final state 64 | * @description if the final state is true, agreement will not change state further anymore 65 | * @return boolean 66 | */ 67 | isFinalState(): boolean { 68 | const state = this.getState(); 69 | return state !== "Pending" && state !== "Proposal"; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/market/agreement/index.ts: -------------------------------------------------------------------------------- 1 | export { Agreement, ProviderInfo, AgreementState } from "./agreement"; 2 | export * from "./agreement-event"; 3 | -------------------------------------------------------------------------------- /src/market/demand/directors/base-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic config utility class 3 | * 4 | * Helps in building more specific config classes 5 | */ 6 | export class BaseConfig { 7 | protected isPositiveInt(value: number) { 8 | return value > 0 && Number.isInteger(value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/market/demand/directors/basic-demand-director-config.test.ts: -------------------------------------------------------------------------------- 1 | import { BasicDemandDirectorConfig } from "./basic-demand-director-config"; 2 | 3 | describe("BasicDemandDirectorConfig", () => { 4 | test("it sets the subnet tag property", () => { 5 | const config = new BasicDemandDirectorConfig({ 6 | subnetTag: "public", 7 | }); 8 | 9 | expect(config.subnetTag).toBe("public"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/market/demand/directors/basic-demand-director-config.ts: -------------------------------------------------------------------------------- 1 | import { BaseConfig } from "./base-config"; 2 | import * as EnvUtils from "../../../shared/utils/env"; 3 | 4 | export interface BasicDemandDirectorConfigOptions { 5 | /** Determines which subnet tag should be used for the offer/demand matching */ 6 | subnetTag: string; 7 | } 8 | 9 | export class BasicDemandDirectorConfig extends BaseConfig implements BasicDemandDirectorConfigOptions { 10 | public readonly subnetTag: string = EnvUtils.getYagnaSubnet(); 11 | 12 | constructor(options?: Partial) { 13 | super(); 14 | 15 | if (options?.subnetTag) { 16 | this.subnetTag = options.subnetTag; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/market/demand/directors/basic-demand-director.ts: -------------------------------------------------------------------------------- 1 | import { DemandBodyBuilder } from "../demand-body-builder"; 2 | import { IDemandDirector } from "../../market.module"; 3 | import { BasicDemandDirectorConfig } from "./basic-demand-director-config"; 4 | 5 | export class BasicDemandDirector implements IDemandDirector { 6 | constructor(private config: BasicDemandDirectorConfig = new BasicDemandDirectorConfig()) {} 7 | 8 | apply(builder: DemandBodyBuilder) { 9 | builder 10 | .addProperty("golem.srv.caps.multi-activity", true) 11 | .addProperty("golem.node.debug.subnet", this.config.subnetTag); 12 | 13 | builder 14 | .addConstraint("golem.com.pricing.model", "linear") 15 | .addConstraint("golem.node.debug.subnet", this.config.subnetTag); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/market/demand/directors/payment-demand-director-config.test.ts: -------------------------------------------------------------------------------- 1 | import { PaymentDemandDirectorConfig } from "./payment-demand-director-config"; 2 | 3 | describe("PaymentDemandDirectorConfig", () => { 4 | it("should throw user error if debitNotesAcceptanceTimeoutSec option is invalid", () => { 5 | expect(() => { 6 | new PaymentDemandDirectorConfig({ 7 | debitNotesAcceptanceTimeoutSec: -3, 8 | }); 9 | }).toThrow("The debit note acceptance timeout time has to be a positive integer"); 10 | }); 11 | 12 | it("should throw user error if midAgreementDebitNoteIntervalSec option is invalid", () => { 13 | expect(() => { 14 | new PaymentDemandDirectorConfig({ 15 | midAgreementDebitNoteIntervalSec: -3, 16 | }); 17 | }).toThrow("The debit note interval time has to be a positive integer"); 18 | }); 19 | 20 | it("should throw user error if midAgreementPaymentTimeoutSec option is invalid", () => { 21 | expect(() => { 22 | new PaymentDemandDirectorConfig({ 23 | midAgreementPaymentTimeoutSec: -3, 24 | }); 25 | }).toThrow("The mid-agreement payment timeout time has to be a positive integer"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/market/demand/directors/payment-demand-director-config.ts: -------------------------------------------------------------------------------- 1 | import { BaseConfig } from "./base-config"; 2 | import { GolemConfigError } from "../../../shared/error/golem-error"; 3 | 4 | export interface PaymentDemandDirectorConfigOptions { 5 | midAgreementDebitNoteIntervalSec: number; 6 | midAgreementPaymentTimeoutSec: number; 7 | debitNotesAcceptanceTimeoutSec: number; 8 | } 9 | 10 | export class PaymentDemandDirectorConfig extends BaseConfig implements PaymentDemandDirectorConfigOptions { 11 | public readonly debitNotesAcceptanceTimeoutSec = 2 * 60; // 2 minutes 12 | public readonly midAgreementDebitNoteIntervalSec = 2 * 60; // 2 minutes 13 | public readonly midAgreementPaymentTimeoutSec = 12 * 60 * 60; // 12 hours 14 | 15 | constructor(options?: Partial) { 16 | super(); 17 | 18 | if (options) { 19 | Object.assign(this, options); 20 | } 21 | 22 | if (!this.isPositiveInt(this.debitNotesAcceptanceTimeoutSec)) { 23 | throw new GolemConfigError("The debit note acceptance timeout time has to be a positive integer"); 24 | } 25 | 26 | if (!this.isPositiveInt(this.midAgreementDebitNoteIntervalSec)) { 27 | throw new GolemConfigError("The debit note interval time has to be a positive integer"); 28 | } 29 | 30 | if (!this.isPositiveInt(this.midAgreementPaymentTimeoutSec)) { 31 | throw new GolemConfigError("The mid-agreement payment timeout time has to be a positive integer"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/market/demand/directors/payment-demand-director.ts: -------------------------------------------------------------------------------- 1 | import { DemandBodyBuilder } from "../demand-body-builder"; 2 | import { IDemandDirector } from "../../market.module"; 3 | import { PaymentDemandDirectorConfig } from "./payment-demand-director-config"; 4 | import { Allocation } from "../../../payment"; 5 | import { IMarketApi } from "../../api"; 6 | 7 | export class PaymentDemandDirector implements IDemandDirector { 8 | constructor( 9 | private allocation: Allocation, 10 | private marketApiAdapter: IMarketApi, 11 | private config: PaymentDemandDirectorConfig = new PaymentDemandDirectorConfig(), 12 | ) {} 13 | 14 | async apply(builder: DemandBodyBuilder) { 15 | // Configure mid-agreement payments 16 | builder 17 | .addProperty("golem.com.scheme.payu.debit-note.interval-sec?", this.config.midAgreementDebitNoteIntervalSec) 18 | .addProperty("golem.com.scheme.payu.payment-timeout-sec?", this.config.midAgreementPaymentTimeoutSec) 19 | .addProperty("golem.com.payment.debit-notes.accept-timeout?", this.config.debitNotesAcceptanceTimeoutSec); 20 | 21 | // Configure payment platform 22 | const { constraints, properties } = await this.marketApiAdapter.getPaymentRelatedDemandDecorations( 23 | this.allocation.id, 24 | ); 25 | builder.mergePrototype({ constraints, properties }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/market/demand/directors/workload-demand-director-config.ts: -------------------------------------------------------------------------------- 1 | import { WorkloadDemandDirectorConfigOptions } from "../options"; 2 | import { GolemConfigError } from "../../../shared/error/golem-error"; 3 | import { BaseConfig } from "./base-config"; 4 | 5 | export enum PackageFormat { 6 | GVMKitSquash = "gvmkit-squash", 7 | } 8 | 9 | type RequiredWorkloadDemandConfigOptions = { 10 | /** Number of seconds after which the agreement resulting from this demand will no longer be valid */ 11 | expirationSec: number; 12 | }; 13 | 14 | export class WorkloadDemandDirectorConfig extends BaseConfig { 15 | readonly packageFormat: string = PackageFormat.GVMKitSquash; 16 | readonly engine: string = "vm"; 17 | readonly runtime = { 18 | name: "vm", 19 | version: undefined, 20 | }; 21 | readonly minMemGib: number = 0.5; 22 | readonly minStorageGib: number = 2; 23 | readonly minCpuThreads: number = 1; 24 | readonly minCpuCores: number = 1; 25 | readonly capabilities: string[] = []; 26 | 27 | readonly expirationSec: number; 28 | 29 | readonly manifest?: string; 30 | readonly manifestSig?: string; 31 | readonly manifestSigAlgorithm?: string; 32 | readonly manifestCert?: string; 33 | readonly useHttps?: boolean = false; 34 | readonly imageHash?: string; 35 | readonly imageTag?: string; 36 | readonly imageUrl?: string; 37 | 38 | constructor(options: Partial & RequiredWorkloadDemandConfigOptions) { 39 | super(); 40 | 41 | Object.assign(this, options); 42 | 43 | if (!options.runtime?.name) { 44 | this.runtime.name = this.engine; 45 | } 46 | 47 | this.expirationSec = options.expirationSec; 48 | 49 | if (!this.imageHash && !this.manifest && !this.imageTag && !this.imageUrl) { 50 | throw new GolemConfigError("You must define a package or manifest option"); 51 | } 52 | 53 | if (this.imageUrl && !this.imageHash) { 54 | throw new GolemConfigError("If you provide an imageUrl, you must also provide it's SHA3-224 hash in imageHash"); 55 | } 56 | 57 | if (!this.isPositiveInt(this.expirationSec)) { 58 | throw new GolemConfigError("The expirationSec param has to be a positive integer"); 59 | } 60 | 61 | if (options.engine && options.runtime) { 62 | throw new GolemConfigError( 63 | "The engine parameter is deprecated and cannot be used with the runtime parameter. Use the runtime option only", 64 | ); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/market/demand/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./demand"; 2 | export * from "./demand-body-builder"; 3 | -------------------------------------------------------------------------------- /src/market/error.ts: -------------------------------------------------------------------------------- 1 | import { GolemModuleError } from "../shared/error/golem-error"; 2 | 3 | export enum MarketErrorCode { 4 | CouldNotGetAgreement = "CouldNotGetAgreement", 5 | CouldNotGetProposal = "CouldNotGetProposal", 6 | ServiceNotInitialized = "ServiceNotInitialized", 7 | MissingAllocation = "MissingAllocation", 8 | SubscriptionFailed = "SubscriptionFailed", 9 | InvalidProposal = "InvalidProposal", 10 | ProposalResponseFailed = "ProposalResponseFailed", 11 | ProposalRejectionFailed = "ProposalRejectionFailed", 12 | DemandExpired = "DemandExpired", 13 | ResourceRentalTerminationFailed = "ResourceRentalTerminationFailed", 14 | ResourceRentalCreationFailed = "ResourceRentalCreationFailed", 15 | AgreementApprovalFailed = "AgreementApprovalFailed", 16 | NoProposalAvailable = "NoProposalAvailable", 17 | InternalError = "InternalError", 18 | ScanFailed = "ScanFailed", 19 | } 20 | 21 | export class GolemMarketError extends GolemModuleError { 22 | constructor( 23 | message: string, 24 | public code: MarketErrorCode, 25 | public previous?: Error, 26 | ) { 27 | super(message, code, previous); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/market/helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockPropertyPolicy, imock, instance, when } from "@johanblumenberg/ts-mockito"; 2 | 3 | import { getHealthyProvidersWhiteList } from "./helpers"; 4 | import { GolemInternalError } from "../shared/error/golem-error"; 5 | 6 | const mockFetch = jest.spyOn(global, "fetch"); 7 | const response = imock(); 8 | 9 | beforeEach(() => { 10 | jest.resetAllMocks(); 11 | }); 12 | 13 | describe("Market Helpers", () => { 14 | describe("Getting public healthy providers whitelist", () => { 15 | describe("Positive cases", () => { 16 | test("Will return the list returned by the endpoint", async () => { 17 | // Given 18 | when(response.json()).thenResolve(["0xAAA", "0xBBB"]); 19 | mockFetch.mockResolvedValue(instance(response)); 20 | 21 | // When 22 | const data = await getHealthyProvidersWhiteList(); 23 | 24 | // Then 25 | expect(data).toEqual(["0xAAA", "0xBBB"]); 26 | }); 27 | }); 28 | 29 | describe("Negative cases", () => { 30 | test("It throws an error when the response from the API will not be a successful one (fetch -> response.ok)", async () => { 31 | // Given 32 | const mockResponse = imock(MockPropertyPolicy.StubAsProperty); 33 | when(mockResponse.ok).thenReturn(false); 34 | when(mockResponse.text()).thenResolve("{error:'test'}"); 35 | mockFetch.mockResolvedValue(instance(mockResponse)); 36 | 37 | // When, Then 38 | await expect(() => getHealthyProvidersWhiteList()).rejects.toMatchError( 39 | new GolemInternalError( 40 | "Failed to download healthy provider whitelist due to an error: Error: Request to download healthy provider whitelist failed: {error:'test'}", 41 | new GolemInternalError("Request to download healthy provider whitelist failed: {error:'test'}"), 42 | ), 43 | ); 44 | }); 45 | 46 | test("It throws an error when executing of fetch will fail for any reason", async () => { 47 | // Given 48 | const testError = new Error("Something went wrong really bad!"); 49 | mockFetch.mockImplementation(() => { 50 | throw testError; 51 | }); 52 | 53 | // When, Then 54 | await expect(() => getHealthyProvidersWhiteList()).rejects.toMatchError( 55 | new GolemInternalError( 56 | "Failed to download healthy provider whitelist due to an error: Error: Something went wrong really bad!", 57 | testError, 58 | ), 59 | ); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/market/helpers.ts: -------------------------------------------------------------------------------- 1 | import { GolemInternalError } from "../shared/error/golem-error"; 2 | 3 | /** 4 | * Helps to obtain a whitelist of providers which were health-tested. 5 | * 6 | * Important: This helper requires internet access to function properly. 7 | * 8 | * @return An array with Golem Node IDs of the whitelisted providers. 9 | */ 10 | export async function getHealthyProvidersWhiteList(): Promise { 11 | try { 12 | const response = await fetch("https://reputation.golem.network/v1/provider-whitelist"); 13 | 14 | if (response.ok) { 15 | return response.json(); 16 | } else { 17 | const body = await response.text(); 18 | 19 | throw new GolemInternalError(`Request to download healthy provider whitelist failed: ${body}`); 20 | } 21 | } catch (err) { 22 | throw new GolemInternalError(`Failed to download healthy provider whitelist due to an error: ${err}`, err); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/market/index.ts: -------------------------------------------------------------------------------- 1 | export { OfferProposalFilter } from "./proposal/offer-proposal"; 2 | export { Demand, BasicDemandPropertyConfig, DemandSpecification } from "./demand/demand"; 3 | export { OfferProposal, ProposalDTO } from "./proposal/offer-proposal"; 4 | export * as OfferProposalFilterFactory from "./strategy"; 5 | export { GolemMarketError, MarketErrorCode } from "./error"; 6 | export * as MarketHelpers from "./helpers"; 7 | export * from "./draft-offer-proposal-pool"; 8 | export * from "./market.module"; 9 | export * from "./api"; 10 | export * from "./agreement"; 11 | export { BasicDemandDirector } from "./demand/directors/basic-demand-director"; 12 | export { PaymentDemandDirector } from "./demand/directors/payment-demand-director"; 13 | export { WorkloadDemandDirector } from "./demand/directors/workload-demand-director"; 14 | export * from "./proposal/market-proposal-event"; 15 | export * from "./scan"; 16 | -------------------------------------------------------------------------------- /src/market/proposal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./market-proposal-event"; 2 | export * from "./market-proposal"; 3 | export * from "./offer-proposal"; 4 | export * from "./offer-counter-proposal"; 5 | export * from "./proposals_batch"; 6 | export * from "./proposal-properties"; 7 | -------------------------------------------------------------------------------- /src/market/proposal/market-proposal-event.ts: -------------------------------------------------------------------------------- 1 | import { OfferProposal } from "./offer-proposal"; 2 | import { OfferCounterProposal } from "./offer-counter-proposal"; 3 | 4 | export type OfferProposalReceivedEvent = { 5 | type: "ProposalReceived"; 6 | proposal: OfferProposal; 7 | timestamp: Date; 8 | }; 9 | 10 | export type OfferCounterProposalRejectedEvent = { 11 | type: "ProposalRejected"; 12 | counterProposal: OfferCounterProposal; 13 | reason: string; 14 | timestamp: Date; 15 | }; 16 | 17 | export type OfferPropertyQueryReceivedEvent = { 18 | type: "PropertyQueryReceived"; 19 | timestamp: Date; 20 | }; 21 | 22 | export type MarketProposalEvent = 23 | | OfferProposalReceivedEvent 24 | | OfferCounterProposalRejectedEvent 25 | | OfferPropertyQueryReceivedEvent; 26 | -------------------------------------------------------------------------------- /src/market/proposal/market-proposal.ts: -------------------------------------------------------------------------------- 1 | import { ProposalProperties } from "./proposal-properties"; 2 | import { MarketApi } from "ya-ts-client"; 3 | import { ProposalState } from "./offer-proposal"; 4 | import { Demand } from "../demand"; 5 | 6 | export interface IProposalRepository { 7 | add(proposal: MarketProposal): MarketProposal; 8 | 9 | getById(id: string): MarketProposal | undefined; 10 | 11 | getByDemandAndId(demand: Demand, id: string): Promise; 12 | } 13 | 14 | /** 15 | * Base representation of a market proposal that can be issued either by the Provider (offer proposal) 16 | * or Requestor (counter-proposal) 17 | */ 18 | export abstract class MarketProposal { 19 | public readonly id: string; 20 | /** 21 | * Reference to the previous proposal in the "negotiation chain" 22 | * 23 | * If null, this means that was the initial offer that the negotiations started from 24 | */ 25 | public readonly previousProposalId: string | null = null; 26 | 27 | public abstract readonly issuer: "Provider" | "Requestor"; 28 | 29 | public readonly properties: ProposalProperties; 30 | 31 | protected constructor(protected readonly model: MarketApi.ProposalDTO) { 32 | this.id = model.proposalId; 33 | this.previousProposalId = model.prevProposalId ?? null; 34 | this.properties = model.properties as ProposalProperties; 35 | } 36 | 37 | public get state(): ProposalState { 38 | return this.model.state; 39 | } 40 | 41 | public get timestamp(): Date { 42 | return new Date(Date.parse(this.model.timestamp)); 43 | } 44 | 45 | isInitial(): boolean { 46 | return this.model.state === "Initial"; 47 | } 48 | 49 | isDraft(): boolean { 50 | return this.model.state === "Draft"; 51 | } 52 | 53 | isExpired(): boolean { 54 | return this.model.state === "Expired"; 55 | } 56 | 57 | isRejected(): boolean { 58 | return this.model.state === "Rejected"; 59 | } 60 | 61 | public isValid(): boolean { 62 | try { 63 | this.validate(); 64 | return true; 65 | } catch (err) { 66 | return false; 67 | } 68 | } 69 | 70 | protected abstract validate(): void | never; 71 | } 72 | -------------------------------------------------------------------------------- /src/market/proposal/offer-counter-proposal.ts: -------------------------------------------------------------------------------- 1 | import { MarketProposal } from "./market-proposal"; 2 | import { MarketApi } from "ya-ts-client"; 3 | 4 | export class OfferCounterProposal extends MarketProposal { 5 | public readonly issuer = "Requestor"; 6 | 7 | constructor(model: MarketApi.ProposalDTO) { 8 | super(model); 9 | } 10 | 11 | protected validate(): void { 12 | return; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/market/scan/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./scan-director"; 3 | export * from "./scanned-offer"; 4 | -------------------------------------------------------------------------------- /src/market/scan/scanned-offer.test.ts: -------------------------------------------------------------------------------- 1 | import { ScannedOffer } from "./scanned-offer"; 2 | 3 | describe("Scanned Offer", () => { 4 | test("Returns payment platform address information", async () => { 5 | const offer = new ScannedOffer({ 6 | offerId: "example-id", 7 | properties: { 8 | "golem.com.payment.platform.erc20-polygon-glm.address": "0xPolygonAddress", 9 | "golem.com.payment.platform.erc20-holesky-tglm.address": "0xHoleskyAddress", 10 | "golem.com.payment.platform.nonsense": "0xNonsense", 11 | "some.other.prop": "with-a-value", 12 | }, 13 | timestamp: new Date().toISOString(), 14 | providerId: "provider-id", 15 | constraints: "", 16 | }); 17 | 18 | expect(offer.paymentPlatformAddresses["erc20-polygon-glm"]).toEqual("0xPolygonAddress"); 19 | expect(offer.paymentPlatformAddresses["erc20-holesky-tglm"]).toEqual("0xHoleskyAddress"); 20 | expect(Object.entries(offer.paymentPlatformAddresses).length).toEqual(2); 21 | }); 22 | 23 | test("Provides API to get cost estimate", () => { 24 | const durationHours = 1; 25 | 26 | const hr2Sec = (hours: number) => hours * 60 * 60; 27 | 28 | const numThreads = 4; 29 | const startPrice = 0.3; 30 | const envPerSec = 0.2; 31 | const cpuPerSec = 0.1; 32 | 33 | const offer = new ScannedOffer({ 34 | offerId: "example-id", 35 | properties: { 36 | "golem.com.payment.platform.erc20-polygon-glm.address": "0xPolygonAddress", 37 | "golem.com.payment.platform.erc20-holesky-tglm.address": "0xHoleskyAddress", 38 | "golem.com.payment.platform.nonsense": "0xNonsense", 39 | "golem.com.usage.vector": ["golem.usage.cpu_sec", "golem.usage.duration_sec"], 40 | "golem.com.pricing.model.linear.coeffs": [cpuPerSec, envPerSec, startPrice], 41 | "golem.inf.cpu.threads": numThreads, 42 | "some.other.prop": "with-a-value", 43 | }, 44 | timestamp: new Date().toISOString(), 45 | providerId: "provider-id", 46 | constraints: "", 47 | }); 48 | 49 | expect(offer.getEstimatedCost(durationHours)).toEqual( 50 | startPrice + numThreads * hr2Sec(durationHours) * cpuPerSec + hr2Sec(durationHours) * envPerSec, 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/market/scan/types.ts: -------------------------------------------------------------------------------- 1 | export type ScanOptions = { 2 | workload?: { 3 | /** 4 | * @deprecated This param is deprecated and will be removed in future versions. Please use the 'runtime.name' instead. 5 | */ 6 | engine?: string; 7 | runtime?: { 8 | name?: string; 9 | version?: string; 10 | }; 11 | capabilities?: string[]; 12 | minMemGib?: number; 13 | maxMemGib?: number; 14 | minStorageGib?: number; 15 | maxStorageGib?: number; 16 | minCpuThreads?: number; 17 | maxCpuThreads?: number; 18 | minCpuCores?: number; 19 | maxCpuCores?: number; 20 | }; 21 | subnetTag?: string; 22 | payment?: { 23 | network: string; 24 | /** @default erc20 */ 25 | driver?: string; 26 | /** @default "glm" if network is mainnet or polygon, "tglm" otherwise */ 27 | token?: string; 28 | }; 29 | }; 30 | 31 | export type ScanSpecification = { 32 | constraints: string[]; 33 | }; 34 | -------------------------------------------------------------------------------- /src/market/strategy.ts: -------------------------------------------------------------------------------- 1 | import { OfferProposal } from "./proposal/offer-proposal"; 2 | 3 | /** Default Proposal filter that accept all proposal coming from the market */ 4 | export const acceptAll = () => () => true; 5 | 6 | /** Proposal filter blocking every offer coming from a provider whose id is in the array */ 7 | export const disallowProvidersById = (providerIds: string[]) => (proposal: OfferProposal) => 8 | !providerIds.includes(proposal.provider.id); 9 | 10 | /** Proposal filter blocking every offer coming from a provider whose name is in the array */ 11 | export const disallowProvidersByName = (providerNames: string[]) => (proposal: OfferProposal) => 12 | !providerNames.includes(proposal.provider.name); 13 | 14 | /** Proposal filter blocking every offer coming from a provider whose name match to the regexp */ 15 | export const disallowProvidersByNameRegex = (regexp: RegExp) => (proposal: OfferProposal) => 16 | !proposal.provider.name.match(regexp); 17 | 18 | /** Proposal filter that only allows offers from a provider whose id is in the array */ 19 | export const allowProvidersById = (providerIds: string[]) => (proposal: OfferProposal) => 20 | providerIds.includes(proposal.provider.id); 21 | 22 | /** Proposal filter that only allows offers from a provider whose name is in the array */ 23 | export const allowProvidersByName = (providerNames: string[]) => (proposal: OfferProposal) => 24 | providerNames.includes(proposal.provider.name); 25 | 26 | /** Proposal filter that only allows offers from a provider whose name match to the regexp */ 27 | export const allowProvidersByNameRegex = (regexp: RegExp) => (proposal: OfferProposal) => 28 | !!proposal.provider.name.match(regexp); 29 | 30 | export type PriceLimits = { 31 | start: number; 32 | cpuPerSec: number; 33 | envPerSec: number; 34 | }; 35 | 36 | /** 37 | * Proposal filter only allowing offers that do not exceed the defined usage 38 | * 39 | * @param priceLimits.start The maximum start price in GLM 40 | * @param priceLimits.cpuPerSec The maximum price for CPU usage in GLM/s 41 | * @param priceLimits.envPerSec The maximum price for the duration of the activity in GLM/s 42 | */ 43 | export const limitPriceFilter = (priceLimits: PriceLimits) => (proposal: OfferProposal) => { 44 | return ( 45 | proposal.pricing.cpuSec <= priceLimits.cpuPerSec && 46 | proposal.pricing.envSec <= priceLimits.envPerSec && 47 | proposal.pricing.start <= priceLimits.start 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/network/api.ts: -------------------------------------------------------------------------------- 1 | import { Network } from "./network"; 2 | import { NetworkNode } from "./node"; 3 | import { NetworkOptions } from "./network.module"; 4 | 5 | export interface NetworkEvents { 6 | networkCreated: (event: { network: Network }) => void; 7 | errorCreatingNetwork: (event: { error: Error }) => void; 8 | 9 | networkRemoved: (event: { network: Network }) => void; 10 | errorRemovingNetwork: (event: { network: Network; error: Error }) => void; 11 | 12 | nodeCreated: (event: { network: Network; node: NetworkNode }) => void; 13 | errorCreatingNode: (event: { network: Network; error: Error }) => void; 14 | 15 | nodeRemoved: (event: { network: Network; node: NetworkNode }) => void; 16 | errorRemovingNode: (event: { network: Network; node: NetworkNode; error: Error }) => void; 17 | } 18 | 19 | export interface INetworkApi { 20 | /** 21 | * Creates a new network with the specified options. 22 | * @param options NetworkOptions 23 | */ 24 | createNetwork(options: NetworkOptions): Promise; 25 | 26 | /** 27 | * Removes an existing network. 28 | * @param network - The network to be removed. 29 | */ 30 | removeNetwork(network: Network): Promise; 31 | 32 | /** 33 | * Creates a new node within a specified network. 34 | * @param network - The network to which the node will be added. 35 | * @param nodeId - The ID of the node to be created. 36 | * @param nodeIp - Optional IP address for the node. If not provided, the first available IP address will be assigned. 37 | */ 38 | 39 | createNetworkNode(network: Network, nodeId: string, nodeIp?: string): Promise; 40 | 41 | /** 42 | * Removes an existing node from a specified network. 43 | * @param network - The network from which the node will be removed. 44 | * @param node - The node to be removed. 45 | */ 46 | removeNetworkNode(network: Network, node: NetworkNode): Promise; 47 | 48 | /** 49 | * Returns the identifier of the requesor 50 | */ 51 | getIdentity(): Promise; 52 | } 53 | -------------------------------------------------------------------------------- /src/network/error.ts: -------------------------------------------------------------------------------- 1 | import { GolemModuleError } from "../shared/error/golem-error"; 2 | import { NetworkInfo } from "./network"; 3 | 4 | export enum NetworkErrorCode { 5 | ServiceNotInitialized = "ServiceNotInitialized", 6 | NetworkSetupMissing = "NetworkSetupMissing", 7 | NetworkCreationFailed = "NetworkCreationFailed", 8 | NoAddressesAvailable = "NoAddressesAvailable", 9 | AddressOutOfRange = "AddressOutOfRange", 10 | AddressAlreadyAssigned = "AddressAlreadyAssigned", 11 | NodeAddingFailed = "NodeAddingFailed", 12 | NodeRemovalFailed = "NodeRemovalFailed", 13 | NetworkRemovalFailed = "NetworkRemovalFailed", 14 | GettingIdentityFailed = "GettingIdentityFailed", 15 | NetworkRemoved = "NetworkRemoved", 16 | } 17 | 18 | export class GolemNetworkError extends GolemModuleError { 19 | #network?: NetworkInfo; 20 | constructor( 21 | message: string, 22 | public code: NetworkErrorCode, 23 | network?: NetworkInfo, 24 | public previous?: Error, 25 | ) { 26 | super(message, code, previous); 27 | this.#network = network; 28 | } 29 | public getNetwork(): NetworkInfo | undefined { 30 | return this.#network; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/network/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./network"; 2 | export * from "./node"; 3 | export * from "./network.module"; 4 | export * from "./error"; 5 | export * from "./api"; 6 | -------------------------------------------------------------------------------- /src/network/node.ts: -------------------------------------------------------------------------------- 1 | import { NetworkInfo } from "./network"; 2 | import { DeployArgs } from "../activity/script/command"; 3 | 4 | /** 5 | * Describes a node in a VPN, mapping a Golem node id to an IP address 6 | */ 7 | export class NetworkNode { 8 | constructor( 9 | public readonly id: string, 10 | public readonly ip: string, 11 | public getNetworkInfo: () => NetworkInfo, 12 | public yagnaBaseUri: string, 13 | ) {} 14 | 15 | /** 16 | * Generate a dictionary of arguments that are required for the appropriate 17 | *`Deploy` command of an exe-script in order to pass the network configuration to the runtime 18 | * on the provider's end. 19 | */ 20 | getNetworkDeploymentArg(): Pick { 21 | return { 22 | net: [ 23 | { 24 | ...this.getNetworkInfo(), 25 | nodeIp: this.ip, 26 | }, 27 | ], 28 | }; 29 | } 30 | 31 | getWebsocketUri(port: number): string { 32 | const url = new URL(this.yagnaBaseUri); 33 | url.protocol = "ws"; 34 | return `${url.href}/net/${this.getNetworkInfo().id}/tcp/${this.ip}/${port}`; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/network/tcp-proxy.test.ts: -------------------------------------------------------------------------------- 1 | jest.mock("../shared/utils", () => ({ 2 | checkAndThrowUnsupportedInBrowserError: () => { 3 | throw new GolemUserError("Not supported in browser"); 4 | }, 5 | })); 6 | 7 | import { GolemUserError } from "../shared/error/golem-error"; 8 | 9 | import { TcpProxy } from "./tcp-proxy"; 10 | 11 | describe("TCP Proxy in browser", () => { 12 | test("Uses the checkAndThrowUnsupportedInBrowserError util to throw when the function detects browser environment", () => { 13 | expect(() => new TcpProxy("ws://fake.url", "fake-app-key")).toThrow("Not supported in browser"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/payment/BaseDocument.ts: -------------------------------------------------------------------------------- 1 | import { PaymentApi } from "ya-ts-client"; 2 | import { ProviderInfo } from "../market/agreement"; 3 | 4 | export interface BaseModel { 5 | issuerId: string; 6 | recipientId: string; 7 | payeeAddr: string; 8 | payerAddr: string; 9 | paymentPlatform: string; 10 | agreementId: string; 11 | paymentDueDate?: string; 12 | status: PaymentApi.InvoiceDTO["status"]; 13 | } 14 | 15 | /** 16 | * Common properties and methods for payment related documents - Invoices and DebitNotes 17 | */ 18 | export abstract class BaseDocument { 19 | public readonly recipientId: string; 20 | public readonly payeeAddr: string; 21 | public readonly requestorWalletAddress: string; 22 | public readonly paymentPlatform: string; 23 | public readonly agreementId: string; 24 | public readonly paymentDueDate?: string; 25 | 26 | protected status: PaymentApi.InvoiceDTO["status"]; 27 | 28 | protected constructor( 29 | public readonly id: string, 30 | protected model: ModelType, 31 | public readonly provider: ProviderInfo, 32 | ) { 33 | this.recipientId = model.recipientId; 34 | this.payeeAddr = model.payeeAddr; 35 | this.requestorWalletAddress = model.payerAddr; 36 | this.paymentPlatform = model.paymentPlatform; 37 | this.agreementId = model.agreementId; 38 | this.paymentDueDate = model.paymentDueDate; 39 | this.status = model.status; 40 | } 41 | 42 | /** 43 | * Tells what's the current status of the document 44 | */ 45 | public getStatus() { 46 | return this.status; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/payment/PayerDetails.ts: -------------------------------------------------------------------------------- 1 | export class PayerDetails { 2 | constructor( 3 | public readonly network: string, 4 | public readonly driver: string, 5 | public readonly address: string, 6 | // eslint-disable-next-line @typescript-eslint/ban-types -- keep the autocomplete for "glm" and "tglm" but allow any string 7 | public readonly token: "glm" | "tglm" | (string & {}), 8 | ) {} 9 | 10 | getPaymentPlatform() { 11 | return `${this.driver}-${this.network}-${this.token}`; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/payment/allocation.test.ts: -------------------------------------------------------------------------------- 1 | import { Allocation, GolemConfigError, YagnaApi } from "../index"; 2 | import { anything, imock, instance, mock, reset, when } from "@johanblumenberg/ts-mockito"; 3 | import { PaymentApi } from "ya-ts-client"; 4 | 5 | const mockYagna = mock(YagnaApi); 6 | const mockPayment = mock(PaymentApi.RequestorService); 7 | const mockAllocation = imock(); 8 | 9 | describe("Allocation", () => { 10 | beforeEach(() => { 11 | reset(mockYagna); 12 | reset(mockPayment); 13 | reset(mockAllocation); 14 | 15 | when(mockYagna.payment).thenReturn(instance(mockPayment)); 16 | 17 | when(mockPayment.createAllocation(anything())).thenResolve(instance(mockAllocation)); 18 | }); 19 | 20 | describe("Creating", () => { 21 | it("should create allocation", async () => { 22 | const allocation = new Allocation({ 23 | address: "0xSomeAddress", 24 | paymentPlatform: "erc20-holesky-tglm", 25 | allocationId: "allocation-id", 26 | makeDeposit: false, 27 | remainingAmount: "1.0", 28 | spentAmount: "0.0", 29 | timestamp: "2024-01-01T00:00:00.000Z", 30 | totalAmount: "1.0", 31 | }); 32 | expect(allocation).toBeInstanceOf(Allocation); 33 | }); 34 | 35 | it("should not create allocation with empty account parameters", () => { 36 | expect( 37 | () => 38 | new Allocation({ 39 | address: "", 40 | paymentPlatform: "", 41 | allocationId: "allocation-id", 42 | makeDeposit: false, 43 | remainingAmount: "1.0", 44 | spentAmount: "0.0", 45 | timestamp: "2024-01-01T00:00:00.000Z", 46 | totalAmount: "1.0", 47 | }), 48 | ).toThrowError(new GolemConfigError("Account address and payment platform are required")); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/payment/allocation.ts: -------------------------------------------------------------------------------- 1 | import { PaymentApi } from "ya-ts-client"; 2 | import { BasePaymentOptions } from "./config"; 3 | import { GolemConfigError } from "../shared/error/golem-error"; 4 | 5 | export interface AllocationOptions extends BasePaymentOptions { 6 | account: { 7 | address: string; 8 | platform: string; 9 | }; 10 | expirationSec?: number; 11 | } 12 | 13 | /** 14 | * Represents a designated sum of money reserved for the purpose of making some particular payments. Allocations are currently purely virtual objects. An Allocation is connected to a payment account (wallet) specified by address and payment platform field. 15 | */ 16 | export class Allocation { 17 | /** Allocation ID */ 18 | public readonly id: string; 19 | 20 | /** Timestamp of creation */ 21 | public readonly timestamp: string; 22 | 23 | /** Timeout */ 24 | public readonly timeout?: string; 25 | 26 | /** Address of requestor */ 27 | public readonly address: string; 28 | 29 | /** Payment platform */ 30 | public readonly paymentPlatform: string; 31 | 32 | /** Total allocation Amount */ 33 | public readonly totalAmount: string; 34 | 35 | /** The amount that has been already spent */ 36 | public readonly spentAmount: string; 37 | 38 | /** The amount left for spending */ 39 | public readonly remainingAmount: string; 40 | 41 | constructor(private readonly model: PaymentApi.AllocationDTO) { 42 | this.id = model.allocationId; 43 | this.timeout = model.timeout; 44 | this.timestamp = model.timestamp; 45 | this.totalAmount = model.totalAmount; 46 | this.spentAmount = model.spentAmount; 47 | this.remainingAmount = model.remainingAmount; 48 | 49 | if (!model.address || !model.paymentPlatform) { 50 | throw new GolemConfigError("Account address and payment platform are required"); 51 | } 52 | 53 | this.address = model.address; 54 | this.paymentPlatform = model.paymentPlatform; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/payment/config.ts: -------------------------------------------------------------------------------- 1 | import { Logger, YagnaOptions } from "../shared/utils"; 2 | 3 | export interface BasePaymentOptions { 4 | yagnaOptions?: YagnaOptions; 5 | budget?: number; 6 | payment?: { driver?: string; network?: string }; 7 | paymentTimeout?: number; 8 | paymentRequestTimeout?: number; 9 | unsubscribeTimeoutMs?: number; 10 | logger?: Logger; 11 | } 12 | -------------------------------------------------------------------------------- /src/payment/debit_note.spec.ts: -------------------------------------------------------------------------------- 1 | import { DebitNote } from "./debit_note"; 2 | import { imock, instance, mock, reset, when } from "@johanblumenberg/ts-mockito"; 3 | import { YagnaApi } from "../shared/utils"; 4 | import { MarketApi, PaymentApi } from "ya-ts-client"; 5 | import Decimal from "decimal.js-light"; 6 | import { ProviderInfo } from "../market/agreement"; 7 | 8 | const mockYagna = mock(YagnaApi); 9 | const mockPayment = mock(PaymentApi.RequestorService); 10 | const mockMarket = mock(MarketApi.RequestorService); 11 | const mockDebitNote = imock(); 12 | const mockAgreement = imock(); 13 | 14 | const dto: PaymentApi.DebitNoteDTO = { 15 | activityId: "activity-id", 16 | agreementId: "agreement-id", 17 | debitNoteId: "debit-note-id", 18 | issuerId: "provider-node-id", 19 | payeeAddr: "0xRequestorWallet", 20 | payerAddr: "0xProviderWallet", 21 | paymentPlatform: "erc20-polygon-glm", 22 | recipientId: "requestor-node-id", 23 | status: "RECEIVED", 24 | timestamp: "2024-01-01T00.00.000Z", 25 | totalAmountDue: "1", 26 | }; 27 | 28 | const TEST_PROVIDER_INFO: ProviderInfo = { 29 | name: "provider-name", 30 | id: "provider-id", 31 | walletAddress: "0xProviderWallet", 32 | }; 33 | 34 | describe("Debit Notes", () => { 35 | beforeEach(() => { 36 | reset(mockYagna); 37 | reset(mockPayment); 38 | reset(mockMarket); 39 | reset(mockDebitNote); 40 | 41 | when(mockYagna.payment).thenReturn(instance(mockPayment)); 42 | when(mockYagna.market).thenReturn(instance(mockMarket)); 43 | 44 | when(mockDebitNote.debitNoteId).thenReturn("testId"); 45 | when(mockDebitNote.payeeAddr).thenReturn("0x12345"); 46 | when(mockDebitNote.issuerId).thenReturn("0x123"); 47 | when(mockDebitNote.agreementId).thenReturn("agreementId"); 48 | when(mockDebitNote.totalAmountDue).thenReturn("1"); 49 | 50 | when(mockAgreement.agreementId).thenReturn("agreementId"); 51 | when(mockAgreement.offer).thenReturn({ 52 | offerId: "offerId", 53 | providerId: "providerId", 54 | timestamp: new Date().toISOString(), 55 | properties: { ["golem.node.id.name"]: "testProvider" }, 56 | constraints: "", 57 | }); 58 | 59 | when(mockPayment.getDebitNote("testId")).thenResolve(instance(mockDebitNote)); 60 | 61 | when(mockMarket.getAgreement("agreementId")).thenResolve(instance(mockAgreement)); 62 | }); 63 | 64 | describe("creating", () => { 65 | it("should crete debit note", async () => { 66 | const debitNote = new DebitNote(dto, TEST_PROVIDER_INFO); 67 | expect(debitNote.id).toEqual(dto.debitNoteId); 68 | }); 69 | 70 | it("should crete debit note with a big number amount", async () => { 71 | const debitNote = new DebitNote({ ...dto, totalAmountDue: "0.009551938349900001" }, TEST_PROVIDER_INFO); 72 | expect(new Decimal("0.009551938349900001").eq(new Decimal(debitNote.totalAmountDue))).toEqual(true); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/payment/debit_note.ts: -------------------------------------------------------------------------------- 1 | import { PaymentApi } from "ya-ts-client"; 2 | import { ProviderInfo } from "../market/agreement"; 3 | import { BaseDocument } from "./BaseDocument"; 4 | import Decimal from "decimal.js-light"; 5 | 6 | export interface IDebitNoteRepository { 7 | getById(id: string): Promise; 8 | } 9 | 10 | /** 11 | * A Debit Note is an artifact issued by the Provider to the Requestor, in the context of a specific Activity. It is a notification of Total Amount Due incurred by the Activity until the moment the Debit Note is issued. This is expected to be used as trigger for payment in upfront-payment or pay-as-you-go scenarios. NOTE: Only Debit Notes with non-null paymentDueDate are expected to trigger payments. NOTE: Debit Notes flag the current Total Amount Due, which is accumulated from the start of Activity. Debit Notes are expected to trigger payments, therefore payment amount for the newly received Debit Note is expected to be determined by difference of Total Payments for the Agreement vs Total Amount Due. 12 | */ 13 | export class DebitNote extends BaseDocument { 14 | public readonly id: string; 15 | public readonly previousDebitNoteId?: string; 16 | public readonly timestamp: string; 17 | public readonly activityId: string; 18 | public readonly totalAmountDue: string; 19 | public readonly usageCounterVector?: object; 20 | 21 | /** 22 | * 23 | * @param model 24 | * @param providerInfo 25 | */ 26 | public constructor( 27 | protected model: PaymentApi.DebitNoteDTO, 28 | providerInfo: ProviderInfo, 29 | ) { 30 | super(model.debitNoteId, model, providerInfo); 31 | this.id = model.debitNoteId; 32 | this.timestamp = model.timestamp; 33 | this.activityId = model.activityId; 34 | this.totalAmountDue = model.totalAmountDue; 35 | this.usageCounterVector = model.usageCounterVector; 36 | } 37 | 38 | public getPreciseAmount(): Decimal { 39 | return new Decimal(this.totalAmountDue); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/payment/error.ts: -------------------------------------------------------------------------------- 1 | import { GolemModuleError } from "../shared/error/golem-error"; 2 | import { Allocation } from "./allocation"; 3 | import { ProviderInfo } from "../market/agreement"; 4 | 5 | export enum PaymentErrorCode { 6 | AllocationCreationFailed = "AllocationCreationFailed", 7 | MissingAllocation = "MissingAllocation", 8 | PaymentProcessNotInitialized = "PaymentProcessNotInitialized", 9 | AllocationReleaseFailed = "AllocationReleaseFailed", 10 | InvoiceAcceptanceFailed = "InvoiceAcceptanceFailed", 11 | DebitNoteAcceptanceFailed = "DebitNoteAcceptanceFailed", 12 | InvoiceRejectionFailed = "InvoiceRejectionFailed", 13 | DebitNoteRejectionFailed = "DebitNoteRejectionFailed", 14 | CouldNotGetDebitNote = "CouldNotGetDebitNote", 15 | CouldNotGetInvoice = "CouldNotGetInvoice", 16 | PaymentStatusQueryFailed = "PaymentStatusQueryFailed", 17 | AgreementAlreadyPaid = "AgreementAlreadyPaid", 18 | InvoiceAlreadyReceived = "InvoiceAlreadyReceived", 19 | } 20 | export class GolemPaymentError extends GolemModuleError { 21 | #allocation?: Allocation; 22 | #provider?: ProviderInfo; 23 | constructor( 24 | message: string, 25 | public code: PaymentErrorCode, 26 | allocation?: Allocation, 27 | provider?: ProviderInfo, 28 | public previous?: Error, 29 | ) { 30 | super(message, code, previous); 31 | this.#allocation = allocation; 32 | this.#provider = provider; 33 | } 34 | public getAllocation(): Allocation | undefined { 35 | return this.#allocation; 36 | } 37 | public getProvider(): ProviderInfo | undefined { 38 | return this.#provider; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/payment/index.ts: -------------------------------------------------------------------------------- 1 | export { Invoice } from "./invoice"; 2 | export { DebitNote } from "./debit_note"; 3 | export { Allocation } from "./allocation"; 4 | export { Rejection, RejectionReason } from "./rejection"; 5 | export * as PaymentFilters from "./strategy"; 6 | export { GolemPaymentError, PaymentErrorCode } from "./error"; 7 | export { InvoiceProcessor, InvoiceAcceptResult } from "./InvoiceProcessor"; 8 | export * from "./payment.module"; 9 | export * from "./api"; 10 | export { InvoiceFilter, DebitNoteFilter } from "./agreement_payment_process"; 11 | -------------------------------------------------------------------------------- /src/payment/invoice.ts: -------------------------------------------------------------------------------- 1 | import { BasePaymentOptions } from "./config"; 2 | import { PaymentApi } from "ya-ts-client"; 3 | import { ProviderInfo } from "../market/agreement"; 4 | import { BaseDocument } from "./BaseDocument"; 5 | import Decimal from "decimal.js-light"; 6 | 7 | export type InvoiceOptions = BasePaymentOptions; 8 | 9 | export interface IInvoiceRepository { 10 | getById(id: string): Promise; 11 | } 12 | 13 | /** 14 | * An Invoice is an artifact issued by the Provider to the Requestor, in the context of a specific Agreement. It indicates the total Amount owed by the Requestor in this Agreement. No further Debit Notes shall be issued after the Invoice is issued. The issue of Invoice signals the Termination of the Agreement (if it hasn't been terminated already). No Activity execution is allowed after the Invoice is issued. 15 | */ 16 | export class Invoice extends BaseDocument { 17 | /** Activities IDs covered by this Invoice */ 18 | public readonly activityIds?: string[]; 19 | /** Amount in the invoice */ 20 | public readonly amount: string; 21 | /** Invoice creation timestamp */ 22 | public readonly timestamp: string; 23 | /** Recipient ID */ 24 | public readonly recipientId: string; 25 | 26 | /** 27 | * @param model 28 | * @param providerInfo 29 | */ 30 | public constructor( 31 | protected model: PaymentApi.InvoiceDTO, 32 | providerInfo: ProviderInfo, 33 | ) { 34 | super(model.invoiceId, model, providerInfo); 35 | this.activityIds = model.activityIds; 36 | this.amount = model.amount; 37 | this.timestamp = model.timestamp; 38 | this.recipientId = model.recipientId; 39 | } 40 | 41 | public getPreciseAmount(): Decimal { 42 | return new Decimal(this.amount); 43 | } 44 | 45 | /** 46 | * Compares two invoices together and tells if they are the same thing 47 | */ 48 | public isSameAs(invoice: Invoice) { 49 | return this.id === invoice.id && this.amount === invoice.amount && this.agreementId === invoice.agreementId; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/payment/rejection.ts: -------------------------------------------------------------------------------- 1 | export enum RejectionReason { 2 | UnsolicitedService = "UNSOLICITED_SERVICE", 3 | BadService = "BAD_SERVICE", 4 | IncorrectAmount = "INCORRECT_AMOUNT", 5 | RejectedByRequestorFilter = "REJECTED_BY_REQUESTOR_FILTER", 6 | 7 | /** 8 | * Use it when you're processing an event after the agreement reached it's "final state" 9 | * 10 | * By final state we mean: we got an invoice for that agreement 11 | */ 12 | AgreementFinalized = "AGREEMENT_FINALIZED", 13 | } 14 | 15 | export interface Rejection { 16 | rejectionReason: RejectionReason; 17 | totalAmountAccepted: string; 18 | message?: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/payment/service.ts: -------------------------------------------------------------------------------- 1 | import { BasePaymentOptions } from "./config"; 2 | import { DebitNoteFilter, InvoiceFilter } from "./agreement_payment_process"; 3 | 4 | export interface PaymentOptions extends BasePaymentOptions { 5 | /** Interval for checking new invoices */ 6 | invoiceFetchingInterval?: number; 7 | /** Interval for checking new debit notes */ 8 | debitNotesFetchingInterval?: number; 9 | /** Maximum number of invoice events per one fetching */ 10 | maxInvoiceEvents?: number; 11 | /** Maximum number of debit notes events per one fetching */ 12 | maxDebitNotesEvents?: number; 13 | /** A custom filter that checks every debit notes coming from providers */ 14 | debitNotesFilter?: DebitNoteFilter; 15 | /** A custom filter that checks every invoices coming from providers */ 16 | invoiceFilter?: InvoiceFilter; 17 | } 18 | -------------------------------------------------------------------------------- /src/payment/strategy.test.ts: -------------------------------------------------------------------------------- 1 | import { instance, mock, when } from "@johanblumenberg/ts-mockito"; 2 | import { 3 | acceptAllDebitNotesFilter, 4 | acceptAllInvoicesFilter, 5 | acceptMaxAmountDebitNoteFilter, 6 | acceptMaxAmountInvoiceFilter, 7 | } from "./strategy"; 8 | import { DebitNote } from "./debit_note"; 9 | import { Invoice } from "./invoice"; 10 | 11 | describe("SDK provided Payment Filters", () => { 12 | describe("acceptAllDebitNotesFilter", () => { 13 | test("Accepts all debit notes", () => { 14 | const mockDebitNoteDto = mock(DebitNote); 15 | const debitNotes = [instance(mockDebitNoteDto), instance(mockDebitNoteDto)]; 16 | const accepted = debitNotes.filter(acceptAllDebitNotesFilter()); 17 | expect(accepted.length).toEqual(2); 18 | }); 19 | }); 20 | 21 | describe("acceptAllInvoicesFilter", () => { 22 | test("Accepts all invoices", () => { 23 | const mockInvoiceDto = mock(Invoice); 24 | const invoices = [instance(mockInvoiceDto), instance(mockInvoiceDto)]; 25 | const accepted = invoices.filter(acceptAllInvoicesFilter()); 26 | expect(accepted.length).toEqual(2); 27 | }); 28 | }); 29 | 30 | describe("acceptMaxAmountDebitNoteFilter", () => { 31 | test("Accepts debit notes that don't exceed a specified amount", async () => { 32 | const mockDebitNoteDto0 = mock(DebitNote); 33 | when(mockDebitNoteDto0.totalAmountDue).thenReturn("100"); 34 | const mockDebitNoteDto1 = mock(DebitNote); 35 | when(mockDebitNoteDto1.totalAmountDue).thenReturn("200"); 36 | const debitNotes = [instance(mockDebitNoteDto0), instance(mockDebitNoteDto1)]; 37 | 38 | const filter = acceptMaxAmountDebitNoteFilter(150); 39 | const accepted: DebitNote[] = []; 40 | for (const debitNote of debitNotes) { 41 | if (await filter(debitNote)) { 42 | accepted.push(debitNote); 43 | } 44 | } 45 | expect(accepted.length).toEqual(1); 46 | expect(accepted[0].totalAmountDue).toEqual("100"); 47 | }); 48 | }); 49 | 50 | describe("acceptMaxAmountInvoiceFilter", () => { 51 | test("Accepts invoices that don't exceed a specified amount", async () => { 52 | const mockInvoiceDto0 = mock(Invoice); 53 | when(mockInvoiceDto0.amount).thenReturn("100"); 54 | const mockInvoiceDto1 = mock(Invoice); 55 | when(mockInvoiceDto1.amount).thenReturn("200"); 56 | const invoices = [instance(mockInvoiceDto0), instance(mockInvoiceDto1)]; 57 | 58 | const filter = acceptMaxAmountInvoiceFilter(150); 59 | const accepted: Invoice[] = []; 60 | for (const invoice of invoices) { 61 | if (await filter(invoice)) { 62 | accepted.push(invoice); 63 | } 64 | } 65 | expect(accepted.length).toEqual(1); 66 | expect(accepted[0].amount).toEqual("100"); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/payment/strategy.ts: -------------------------------------------------------------------------------- 1 | import { DebitNote } from "./debit_note"; 2 | import { Invoice } from "./invoice"; 3 | import Decimal from "decimal.js-light"; 4 | 5 | /** Default DebitNotes filter that accept all debit notes without any validation */ 6 | export const acceptAllDebitNotesFilter = () => async () => true; 7 | 8 | /** Default Invoices filter that accept all invoices without any validation */ 9 | export const acceptAllInvoicesFilter = () => async () => true; 10 | 11 | /** A custom filter that only accepts debit notes below a given value */ 12 | export const acceptMaxAmountDebitNoteFilter = (maxAmount: number) => async (debitNote: DebitNote) => 13 | new Decimal(debitNote.totalAmountDue).lte(maxAmount); 14 | 15 | /** A custom filter that only accepts invoices below a given value */ 16 | export const acceptMaxAmountInvoiceFilter = (maxAmount: number) => async (invoice: Invoice) => 17 | new Decimal(invoice.amount).lte(maxAmount); 18 | -------------------------------------------------------------------------------- /src/resource-rental/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./resource-rental"; 2 | export * from "./resource-rental-pool"; 3 | export * from "./rental.module"; 4 | -------------------------------------------------------------------------------- /src/shared/cache/CacheService.ts: -------------------------------------------------------------------------------- 1 | export class CacheService { 2 | private readonly storage = new Map(); 3 | 4 | public set(key: string, value: T) { 5 | this.storage.set(key, value); 6 | 7 | return value; 8 | } 9 | 10 | public get(key: string) { 11 | return this.storage.get(key); 12 | } 13 | 14 | public delete(key: string) { 15 | return this.storage.delete(key); 16 | } 17 | 18 | public has(key: string) { 19 | return this.storage.has(key); 20 | } 21 | 22 | public getAll(): T[] { 23 | return [...this.storage.values()]; 24 | } 25 | 26 | public flushAll() { 27 | return this.storage.clear(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/error/golem-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base class for all errors directly thrown by Golem SDK. 3 | */ 4 | export abstract class GolemError extends Error { 5 | constructor( 6 | message: string, 7 | /** 8 | * The previous error, if any, that led to this error. 9 | */ 10 | public readonly previous?: Error, 11 | ) { 12 | super(message); 13 | } 14 | } 15 | 16 | /** 17 | * User-caused errors in the Golem SDK containing logic errors. 18 | * @example you cannot create an activity for an agreement that already expired 19 | */ 20 | export class GolemUserError extends GolemError {} 21 | 22 | /** 23 | * Represents errors related to the user choosing to abort or stop running activities. 24 | * @example CTRL+C abort error 25 | */ 26 | export class GolemAbortError extends GolemUserError {} 27 | 28 | /** 29 | * Represents configuration errors. 30 | * @example Api key not defined 31 | */ 32 | export class GolemConfigError extends GolemUserError {} 33 | 34 | /** 35 | * Represents errors when the SDK encountered an internal error that wasn't handled correctly. 36 | * @example JSON.parse(undefined) -> Error: Unexpected token u in JSON at position 0 37 | */ 38 | export class GolemInternalError extends GolemError {} 39 | 40 | /** 41 | * Represents errors resulting from yagna’s errors or provider failure 42 | * @examples: 43 | * - yagna results with a HTTP 500-error 44 | * - the provider failed to deploy the activity - permission denied when creating the activity on the provider system itself 45 | */ 46 | export class GolemPlatformError extends GolemError {} 47 | 48 | /** 49 | * SDK timeout errors 50 | * @examples: 51 | * - Not receiving any offers within the configured time. 52 | * - The activity not starting within the configured time. 53 | * - The request (task) timing out (started on an activity but didn't finish on time). 54 | * - The request start timing out (the task didn't start within the configured amount of time). 55 | */ 56 | export class GolemTimeoutError extends GolemError {} 57 | 58 | /** 59 | * Module specific errors - Market, Work, Payment. 60 | * Each of the major modules will have its own domain specific root error type, 61 | * additionally containing an error code specific to a given subdomain 62 | */ 63 | export abstract class GolemModuleError extends GolemError { 64 | protected constructor( 65 | message: string, 66 | public code: string | number, 67 | previous?: Error, 68 | ) { 69 | super(message, previous); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/shared/storage/StorageServerAdapter.ts: -------------------------------------------------------------------------------- 1 | import { FileServerEntry, IFileServer } from "../../activity"; 2 | import { GolemConfigError, GolemInternalError } from "../error/golem-error"; 3 | import { StorageProvider } from "./provider"; 4 | import fs from "fs"; 5 | import jsSha3 from "js-sha3"; 6 | 7 | /** 8 | * IFileServer implementation that uses any StorageProvider to serve files. 9 | * Make sure that the storage provider implements the `.publishFile()` method. 10 | */ 11 | class StorageServerAdapter implements IFileServer { 12 | private published = new Map(); 13 | 14 | constructor(private readonly storage: StorageProvider) {} 15 | 16 | async publishFile(sourcePath: string) { 17 | if (!this.storage.isReady()) { 18 | throw new GolemInternalError("The GFTP server as to be initialized before publishing a file "); 19 | } 20 | 21 | if (!fs.existsSync(sourcePath) || fs.lstatSync(sourcePath).isDirectory()) { 22 | throw new GolemConfigError(`File ${sourcePath} does not exist o is a directory`); 23 | } 24 | 25 | const fileUrl = await this.storage.publishFile(sourcePath); 26 | const fileHash = await this.calculateFileHash(sourcePath); 27 | 28 | const entry = { 29 | fileUrl, 30 | fileHash, 31 | }; 32 | 33 | this.published.set(sourcePath, entry); 34 | 35 | return entry; 36 | } 37 | 38 | public isServing() { 39 | return this.published.size !== 0; 40 | } 41 | 42 | getPublishInfo(sourcePath: string) { 43 | return this.published.get(sourcePath); 44 | } 45 | 46 | isFilePublished(sourcePath: string): boolean { 47 | return this.published.has(sourcePath); 48 | } 49 | 50 | private async calculateFileHash(localPath: string): Promise { 51 | const fileStream = fs.createReadStream(localPath); 52 | const hash = jsSha3.sha3_224.create(); 53 | 54 | return new Promise((resolve, reject) => { 55 | fileStream.on("data", (chunk: Buffer) => hash.update(chunk)); 56 | fileStream.on("end", () => resolve(hash.hex())); 57 | fileStream.on("error", (err) => reject(new GolemInternalError(`Error calculating file hash: ${err}`, err))); 58 | }); 59 | } 60 | } 61 | 62 | /** 63 | * @deprecated Use StorageServerAdapter instead. This will be removed in the next major version. 64 | * 65 | * This class provides GFTP based implementation of the IFileServer interface used in the SDK 66 | */ 67 | class GftpServerAdapter extends StorageServerAdapter {} 68 | 69 | export { GftpServerAdapter, StorageServerAdapter }; 70 | -------------------------------------------------------------------------------- /src/shared/storage/default.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketStorageProvider } from "./ws"; 2 | import { Logger, YagnaApi } from "../utils"; 3 | 4 | export function createDefaultStorageProvider(yagnaApi: YagnaApi, logger?: Logger) { 5 | return new WebSocketStorageProvider(yagnaApi, { 6 | logger: logger?.child("storage"), 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/storage/index.ts: -------------------------------------------------------------------------------- 1 | export { StorageProvider } from "./provider"; 2 | export { GftpStorageProvider } from "./gftp"; 3 | export { NullStorageProvider } from "./null"; 4 | export { WebSocketStorageProvider, WebSocketStorageProviderOptions } from "./ws"; 5 | export { createDefaultStorageProvider } from "./default"; 6 | -------------------------------------------------------------------------------- /src/shared/storage/null.ts: -------------------------------------------------------------------------------- 1 | import { StorageProvider, StorageProviderDataCallback } from "./provider"; 2 | import { GolemInternalError } from "../error/golem-error"; 3 | 4 | /** 5 | * Null Storage Provider. 6 | * 7 | * Blocks all storage operations. Any attempt to use storage will result in an error. 8 | * 9 | * This will be the default storage provider if no default storage provider is available 10 | * for the platform the SDK is running on. 11 | * 12 | * @category mid-level 13 | */ 14 | export class NullStorageProvider implements StorageProvider { 15 | close(): Promise { 16 | return Promise.resolve(undefined); 17 | } 18 | 19 | init(): Promise { 20 | return Promise.resolve(undefined); 21 | } 22 | 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | publishData(src: Uint8Array): Promise { 25 | return Promise.reject(new GolemInternalError("NullStorageProvider does not support upload data")); 26 | } 27 | 28 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 29 | publishFile(src: string): Promise { 30 | return Promise.reject(new GolemInternalError("NullStorageProvider does not support upload files")); 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | receiveFile(path: string): Promise { 35 | return Promise.reject(new GolemInternalError("NullStorageProvider does not support download files")); 36 | } 37 | 38 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 39 | receiveData(callback: StorageProviderDataCallback): Promise { 40 | return Promise.reject(new GolemInternalError("NullStorageProvider does not support download data")); 41 | } 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 44 | release(urls: string[]): Promise { 45 | return Promise.resolve(undefined); 46 | } 47 | 48 | isReady(): boolean { 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/shared/storage/provider.ts: -------------------------------------------------------------------------------- 1 | export type StorageProviderDataCallback = (data: Uint8Array) => void; 2 | 3 | export interface StorageProvider { 4 | /** 5 | * Initialize storage provider. 6 | */ 7 | init(): Promise; 8 | 9 | /** 10 | * Tells if the storage provider is ready for use 11 | */ 12 | isReady(): boolean; 13 | 14 | /** 15 | * Close storage provider and release all resources. 16 | */ 17 | close(): Promise; 18 | 19 | /** 20 | * Return allocated resource URL from Yagna of a file to be downloaded. 21 | */ 22 | receiveFile(destPath: string): Promise; 23 | 24 | /** 25 | * Return allocated resource URL from Yagna of a file to be downloaded. 26 | */ 27 | receiveData(callback: StorageProviderDataCallback): Promise; 28 | 29 | /** 30 | * Return allocated resource URL from Yagna of a file to be uploaded. 31 | * @param srcPath 32 | */ 33 | publishFile(srcPath: string): Promise; 34 | 35 | /** 36 | * Return allocated resource URL from Yagna of data to be uploaded. 37 | * @param data 38 | */ 39 | publishData(data: Uint8Array): Promise; 40 | 41 | /** 42 | * Release previously allocated resource URL from Yagna. 43 | * @param urls 44 | */ 45 | release(urls: string[]): Promise; 46 | } 47 | -------------------------------------------------------------------------------- /src/shared/types.ts: -------------------------------------------------------------------------------- 1 | import { StorageProvider } from "./storage"; 2 | 3 | export type DataTransferProtocol = "gftp" | "ws" | StorageProvider; 4 | -------------------------------------------------------------------------------- /src/shared/utils/abortSignal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If provided an AbortSignal, returns it. 3 | * If provided a number, returns an AbortSignal that will be aborted after the specified number of milliseconds. 4 | * If provided undefined, returns an AbortSignal that will never be aborted. 5 | */ 6 | export function createAbortSignalFromTimeout(timeoutOrSignal: number | AbortSignal | undefined) { 7 | if (timeoutOrSignal instanceof AbortSignal) { 8 | return timeoutOrSignal; 9 | } 10 | if (typeof timeoutOrSignal === "number") { 11 | return AbortSignal.timeout(timeoutOrSignal); 12 | } 13 | return new AbortController().signal; 14 | } 15 | 16 | interface AbortEvent extends Event { 17 | target: EventTarget & { reason?: string | Error }; 18 | } 19 | 20 | /** 21 | * Combine multiple AbortSignals into a single signal that will be aborted if any 22 | * of the input signals are aborted. If any of the input signals are already aborted, 23 | * the returned signal will be aborted immediately. 24 | * 25 | * Polyfill for AbortSignal.any(), since it's only available starting in Node 20 26 | * https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static 27 | * 28 | * The function returns a signal and a cleanup function that allows you 29 | * to remove listeners when they are no longer needed. 30 | */ 31 | 32 | export function anyAbortSignal(...signals: AbortSignal[]): { signal: AbortSignal; cleanup: () => void } { 33 | const controller = new AbortController(); 34 | 35 | const onAbort = (ev: Event) => { 36 | if (controller.signal.aborted) return; 37 | const reason = (ev as AbortEvent).target.reason; 38 | controller.abort(reason); 39 | }; 40 | 41 | for (const signal of signals) { 42 | if (signal.aborted) { 43 | controller.abort(signal.reason); 44 | break; 45 | } 46 | signal.addEventListener("abort", onAbort); 47 | } 48 | 49 | const cleanup = () => { 50 | for (const signal of signals) { 51 | signal.removeEventListener("abort", onAbort); 52 | } 53 | }; 54 | 55 | return { signal: controller.signal, cleanup }; 56 | } 57 | -------------------------------------------------------------------------------- /src/shared/utils/acquireQueue.ts: -------------------------------------------------------------------------------- 1 | import { GolemInternalError } from "../error/golem-error"; 2 | import { anyAbortSignal, createAbortSignalFromTimeout } from "./abortSignal"; 3 | 4 | /** 5 | * `Promise.withResolvers` is only available in Node 22.0.0 and later. 6 | */ 7 | function withResolvers() { 8 | let resolve!: (value: T | PromiseLike) => void; 9 | let reject!: (reason: unknown) => void; 10 | const promise = new Promise((_resolve, _reject) => { 11 | resolve = _resolve; 12 | reject = _reject; 13 | }); 14 | return { resolve, reject, promise }; 15 | } 16 | 17 | type Acquire = (item: T) => void; 18 | 19 | /** 20 | * A queue of acquirers waiting for an item. 21 | * use `get` to queue up for the next available item. 22 | * use `put` to give the item to the next acquirer. 23 | */ 24 | export class AcquireQueue { 25 | private queue: Acquire[] = []; 26 | private abortController = new AbortController(); 27 | 28 | /** 29 | * Release (reject) all acquirers. 30 | * Essentially this is a way to reset the queue. 31 | */ 32 | public releaseAll() { 33 | this.abortController.abort(); 34 | this.queue = []; 35 | this.abortController = new AbortController(); 36 | } 37 | 38 | /** 39 | * Queue up for the next available item. 40 | */ 41 | public async get(signalOrTimeout?: number | AbortSignal): Promise { 42 | const { signal, cleanup } = anyAbortSignal( 43 | createAbortSignalFromTimeout(signalOrTimeout), 44 | this.abortController.signal, 45 | ); 46 | signal.throwIfAborted(); 47 | const { resolve, promise } = withResolvers(); 48 | this.queue.push(resolve); 49 | 50 | const abortPromise = new Promise((_, reject) => { 51 | signal.addEventListener("abort", () => { 52 | this.queue = this.queue.filter((r) => r !== resolve); 53 | reject(signal.reason); 54 | }); 55 | }); 56 | return Promise.race([promise, abortPromise]).finally(cleanup); 57 | } 58 | 59 | /** 60 | * Are there any acquirers waiting for an item? 61 | */ 62 | public hasAcquirers() { 63 | return this.queue.length > 0; 64 | } 65 | 66 | /** 67 | * Give the item to the next acquirer. 68 | * If there are no acquirers, throw an error. You should check `hasAcquirers` before calling this method. 69 | */ 70 | public put(item: T) { 71 | if (!this.hasAcquirers()) { 72 | throw new GolemInternalError("No acquirers waiting for the item"); 73 | } 74 | const resolve = this.queue.shift()!; 75 | resolve(item); 76 | } 77 | 78 | public size() { 79 | return this.queue.length; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/shared/utils/apiErrorMessage.ts: -------------------------------------------------------------------------------- 1 | import YaTsClient from "ya-ts-client"; 2 | function isApiError(error: unknown): error is YaTsClient.ActivityApi.ApiError { 3 | return typeof error == "object" && error !== null && "name" in error && error.name === "ApiError"; 4 | } 5 | /** 6 | * Try to extract a message from a yagna API error. 7 | * If the error is not an instance of `ApiError`, return the error message. 8 | */ 9 | export function getMessageFromApiError(error: unknown): string { 10 | if (!(error instanceof Error)) { 11 | return String(error); 12 | } 13 | 14 | if (isApiError(error)) { 15 | try { 16 | return JSON.stringify(error.body, null, 2); 17 | } catch (_jsonParseError) { 18 | return error.message; 19 | } 20 | } 21 | return error.message; 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/utils/env.test.ts: -------------------------------------------------------------------------------- 1 | import { EnvUtils } from "./index"; 2 | 3 | describe("EnvUtils", () => { 4 | describe("getYagnaApiUrl()", () => { 5 | describe("with env", () => { 6 | let oldUrl: string | undefined; 7 | 8 | beforeEach(() => { 9 | oldUrl = process.env.YAGNA_API_URL; 10 | process.env.YAGNA_API_URL = "TEST"; 11 | }); 12 | 13 | afterEach(() => { 14 | if (typeof oldUrl === "undefined") { 15 | delete process.env.YAGNA_API_URL; 16 | } else { 17 | process.env.YAGNA_API_URL = oldUrl; 18 | } 19 | }); 20 | 21 | it("should use process.env if available", () => { 22 | expect(EnvUtils.getYagnaApiUrl()).toEqual("TEST"); 23 | }); 24 | 25 | it("should return default if env is missing", () => { 26 | delete process.env.YAGNA_API_URL; 27 | expect(EnvUtils.getYagnaApiUrl()).toEqual("http://127.0.0.1:7465"); 28 | }); 29 | }); 30 | 31 | describe("without env", () => { 32 | let process: NodeJS.Process | undefined; 33 | 34 | beforeEach(() => { 35 | process = global.process; 36 | global.process = undefined as unknown as NodeJS.Process; 37 | }); 38 | 39 | afterEach(() => { 40 | global.process = process as NodeJS.Process; 41 | }); 42 | 43 | it("should return default value", () => { 44 | expect(EnvUtils.getYagnaApiUrl()).toEqual("http://127.0.0.1:7465"); 45 | }); 46 | }); 47 | }); 48 | 49 | describe("getYagnaAppKey()", () => { 50 | describe("with env", () => { 51 | let oldKey: string | undefined; 52 | 53 | beforeEach(() => { 54 | oldKey = process.env.YAGNA_APPKEY; 55 | process.env.YAGNA_APPKEY = "TEST"; 56 | }); 57 | 58 | afterEach(() => { 59 | if (typeof oldKey === "undefined") { 60 | delete process.env.YAGNA_APPKEY; 61 | } else { 62 | process.env.YAGNA_APPKEY = oldKey; 63 | } 64 | }); 65 | 66 | it("should use process.env if available", () => { 67 | expect(EnvUtils.getYagnaAppKey()).toEqual("TEST"); 68 | }); 69 | 70 | it("should return empty string if var is missing", () => { 71 | delete process.env.YAGNA_APPKEY; 72 | expect(EnvUtils.getYagnaAppKey()).toEqual(""); 73 | }); 74 | }); 75 | 76 | describe("without env", () => { 77 | let process: NodeJS.Process | undefined; 78 | 79 | beforeEach(() => { 80 | process = global.process; 81 | global.process = undefined as unknown as NodeJS.Process; 82 | }); 83 | 84 | afterEach(() => { 85 | global.process = process as NodeJS.Process; 86 | }); 87 | 88 | it("should return empty string", () => { 89 | expect(EnvUtils.getYagnaAppKey()).toEqual(""); 90 | }); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/shared/utils/env.ts: -------------------------------------------------------------------------------- 1 | import { isNode } from "./runtimeContextChecker"; 2 | 3 | export function getYagnaApiUrl(): string { 4 | return (isNode ? process?.env.YAGNA_API_URL : "") || "http://127.0.0.1:7465"; 5 | } 6 | 7 | export function getYagnaAppKey(): string { 8 | return isNode ? (process?.env.YAGNA_APPKEY ?? "") : ""; 9 | } 10 | 11 | export function getYagnaSubnet(): string { 12 | return isNode ? (process?.env.YAGNA_SUBNET ?? "public") : "public"; 13 | } 14 | 15 | export function getRepoUrl(): string { 16 | return isNode 17 | ? (process?.env.GOLEM_REGISTRY_URL ?? "https://registry.golem.network") 18 | : "https://registry.golem.network"; 19 | } 20 | 21 | export function getPaymentNetwork(): string { 22 | return isNode ? (process.env.PAYMENT_NETWORK ?? "holesky") : "holesky"; 23 | } 24 | 25 | export function isDevMode(): boolean { 26 | return isNode ? process?.env.GOLEM_DEV_MODE === "true" : false; 27 | } 28 | -------------------------------------------------------------------------------- /src/shared/utils/eventLoop.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run a callback on the next event loop iteration ("promote" a microtask to a task using setTimeout). 3 | * Note that this is not guaranteed to run on the very next iteration, but it will run as soon as possible. 4 | * This function is designed to avoid the problem of microtasks queueing other microtasks in an infinite loop. 5 | * See the example below for a common pitfall that this function can help avoid. 6 | * Learn more about microtasks and their relation to async/await here: 7 | * https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth 8 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#control_flow_effects_of_await 9 | * @param cb The callback to run on the next event loop iteration. 10 | * @example 11 | * ```ts 12 | * const signal = AbortSignal.timeout(1_000); 13 | * // This loop will run for 1 second, then stop. 14 | * while (!signal.aborted) { 15 | * await runOnNextEventLoopIteration(() => Promise.resolve()); 16 | * } 17 | * 18 | * const signal = AbortSignal.timeout(1_000); 19 | * // This loop will run indefinitely. 20 | * // Each while loop iteration queues a microtask, which itself queues another microtask, and so on. 21 | * while (!signal.aborted) { 22 | * await Promise.resolve(); 23 | * } 24 | * ``` 25 | */ 26 | export function runOnNextEventLoopIteration(cb: () => Promise): Promise { 27 | return new Promise((resolve, reject) => { 28 | setTimeout(() => cb().then(resolve).catch(reject)); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/shared/utils/index.ts: -------------------------------------------------------------------------------- 1 | import sleep from "./sleep"; 2 | 3 | export { sleep }; 4 | export * from "./runtimeContextChecker"; 5 | export { Logger } from "./logger/logger"; 6 | export { nullLogger } from "./logger/nullLogger"; 7 | export { defaultLogger } from "./logger/defaultLogger"; 8 | export * as EnvUtils from "./env"; 9 | export { YagnaApi, YagnaOptions } from "../yagna/yagnaApi"; 10 | export * from "./abortSignal"; 11 | export * from "./eventLoop"; 12 | export * from "./rxjs"; 13 | export * from "./wait"; 14 | -------------------------------------------------------------------------------- /src/shared/utils/logger/defaultLogger.ts: -------------------------------------------------------------------------------- 1 | import debugLogger from "debug"; 2 | import { Logger } from "./logger"; 3 | 4 | export type DefaultLoggerOptions = { 5 | /** 6 | * Disables prefixing the root namespace with golem-js 7 | * 8 | * @default false 9 | */ 10 | disableAutoPrefix: boolean; 11 | }; 12 | 13 | function getNamespace(namespace: string, disablePrefix: boolean) { 14 | if (disablePrefix) { 15 | return namespace; 16 | } else { 17 | return namespace.startsWith("golem-js:") ? namespace : `golem-js:${namespace}`; 18 | } 19 | } 20 | 21 | /** 22 | * Creates a logger that uses the debug library. This logger is used by default by all entities in the SDK. 23 | * 24 | * If the namespace is not prefixed with `golem-js:`, it will be prefixed automatically - this can be controlled by `disableAutoPrefix` options. 25 | */ 26 | export function defaultLogger( 27 | namespace: string, 28 | opts: DefaultLoggerOptions = { 29 | disableAutoPrefix: false, 30 | }, 31 | ): Logger { 32 | const namespaceWithBase = getNamespace(namespace, opts.disableAutoPrefix); 33 | const logger = debugLogger(namespaceWithBase); 34 | 35 | function log(level: string, msg: string, ctx?: Record | Error) { 36 | if (ctx) { 37 | logger(`[${level}] ${msg} %o`, ctx); 38 | } else { 39 | logger(`[${level}] ${msg}`); 40 | } 41 | } 42 | 43 | function debug(msg: string): void; 44 | function debug(msg: string, ctx?: Record | Error) { 45 | log("DEBUG", msg, ctx); 46 | } 47 | 48 | function info(msg: string): void; 49 | function info(msg: string, ctx?: Record | Error) { 50 | log("INFO", msg, ctx); 51 | } 52 | 53 | function warn(msg: string): void; 54 | function warn(msg: string, ctx?: Record | Error) { 55 | log("WARN", msg, ctx); 56 | } 57 | 58 | function error(msg: string): void; 59 | function error(msg: string, ctx?: Record | Error) { 60 | log("ERROR", msg, ctx); 61 | } 62 | 63 | return { 64 | child: (childNamespace: string) => defaultLogger(`${namespaceWithBase}:${childNamespace}`, opts), 65 | info, 66 | error, 67 | warn, 68 | debug, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/shared/utils/logger/logger.ts: -------------------------------------------------------------------------------- 1 | export interface Logger { 2 | child(namespace: string): Logger; 3 | debug(msg: string): void; 4 | debug(msg: string, ctx: Record | Error | unknown): void; 5 | info(msg: string): void; 6 | info(msg: string, ctx: Record | Error | unknown): void; 7 | warn(msg: string): void; 8 | warn(msg: string, ctx: Record | Error | unknown): void; 9 | error(msg: string): void; 10 | error(msg: string, ctx: Record | Error | unknown): void; 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/utils/logger/nullLogger.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "./logger"; 2 | 3 | export function nullLogger(): Logger { 4 | const nullFunc = () => { 5 | // Do nothing. 6 | }; 7 | 8 | return { 9 | child: () => nullLogger(), 10 | debug: nullFunc, 11 | info: nullFunc, 12 | warn: nullFunc, 13 | error: nullFunc, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/shared/utils/runtimeContextChecker.ts: -------------------------------------------------------------------------------- 1 | import { GolemInternalError } from "../error/golem-error"; 2 | 3 | /** 4 | * @ignore 5 | */ 6 | export const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; 7 | 8 | export const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null; 9 | /** 10 | * @ignore 11 | */ 12 | export const isWebWorker = 13 | typeof self === "object" && self.constructor && self.constructor.name === "DedicatedWorkerGlobalScope"; 14 | /** 15 | * @ignore 16 | */ 17 | export function checkAndThrowUnsupportedInBrowserError(feature: string) { 18 | if (isBrowser) throw new GolemInternalError(`Feature ${feature} is unsupported in the browser.`); 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/utils/rxjs.ts: -------------------------------------------------------------------------------- 1 | import { Observable, Subject, finalize, mergeWith, takeUntil } from "rxjs"; 2 | 3 | /** 4 | * Merges two observables until the first one completes (or errors). 5 | * The difference between this and `merge` is that this will complete when the first observable completes, 6 | * while `merge` would only complete when _all_ observables complete. 7 | */ 8 | export function mergeUntilFirstComplete( 9 | observable1: Observable, 10 | observable2: Observable, 11 | ): Observable { 12 | const completionSubject = new Subject(); 13 | const observable1WithCompletion = observable1.pipe( 14 | takeUntil(completionSubject), 15 | finalize(() => completionSubject.next()), 16 | ); 17 | const observable2WithCompletion = observable2.pipe( 18 | takeUntil(completionSubject), 19 | finalize(() => completionSubject.next()), 20 | ); 21 | 22 | return observable1WithCompletion.pipe(mergeWith(observable2WithCompletion)); 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param time 3 | * @param inMs 4 | * @ignore 5 | */ 6 | const sleep = (time: number, inMs = false): Promise => 7 | new Promise((resolve) => setTimeout(resolve, time * (inMs ? 1 : 1000))); 8 | export default sleep; 9 | -------------------------------------------------------------------------------- /src/shared/utils/timeout.ts: -------------------------------------------------------------------------------- 1 | import { GolemTimeoutError } from "../error/golem-error"; 2 | 3 | export async function withTimeout(promise: Promise, timeoutMs: number): Promise { 4 | let timeoutId: NodeJS.Timeout; 5 | const timeout = (milliseconds: number): Promise => 6 | new Promise((_, reject) => { 7 | timeoutId = setTimeout( 8 | () => reject(new GolemTimeoutError("Timeout for the operation was reached")), 9 | milliseconds, 10 | ); 11 | }); 12 | return Promise.race([promise, timeout(timeoutMs)]).finally(() => clearTimeout(timeoutId)); 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/utils/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * RequireAtLeastOne 3 | * 4 | * From https://stackoverflow.com/a/49725198/435093 5 | * 6 | * This type is used to make sure that at least one of the fields is present. 7 | * 8 | * For example, in the PackageOptions type, we want to make sure that at least one of the fields 9 | * imageHash, imageTag, or manifest is present. 10 | */ 11 | 12 | export type RequireAtLeastOne = Pick> & 13 | { 14 | [K in Keys]-?: Required> & Partial>>; 15 | }[Keys]; 16 | 17 | /** 18 | * Utility type extracting the type of the element of a typed array 19 | */ 20 | export type ElementOf = T extends Array ? U : never; 21 | -------------------------------------------------------------------------------- /src/shared/utils/wait.ts: -------------------------------------------------------------------------------- 1 | import { GolemAbortError } from "../error/golem-error"; 2 | 3 | /** 4 | * Utility function that helps to block the execution until a condition is met (check returns true) or the timeout happens. 5 | * 6 | * @param {function} check - The function checking if the condition is met. 7 | * @param {Object} [opts] - Options controlling the timeout and check interval in seconds. 8 | * @param {AbortSignal} [opts.abortSignal] - AbortSignal to respect when waiting for the condition to be met 9 | * @param {number} [opts.intervalSeconds=1] - The interval between condition checks in seconds. 10 | * 11 | * @return {Promise} - Resolves when the condition is met or rejects with a timeout error if it wasn't met on time. 12 | */ 13 | export function waitFor( 14 | check: () => boolean | Promise, 15 | opts?: { abortSignal?: AbortSignal; intervalSeconds?: number }, 16 | ): Promise { 17 | const intervalSeconds = opts?.intervalSeconds ?? 1; 18 | 19 | let verifyInterval: NodeJS.Timeout | undefined; 20 | 21 | const verify = new Promise((resolve, reject) => { 22 | verifyInterval = setInterval(async () => { 23 | if (opts?.abortSignal?.aborted) { 24 | reject(new GolemAbortError("Waiting for a condition has been aborted", opts.abortSignal.reason)); 25 | } 26 | 27 | if (await check()) { 28 | resolve(); 29 | } 30 | }, intervalSeconds * 1000); 31 | }); 32 | 33 | return verify.finally(() => { 34 | clearInterval(verifyInterval); 35 | }); 36 | } 37 | 38 | /** 39 | * Simple utility that allows you to wait n-seconds and then call the provided function 40 | */ 41 | export function waitAndCall(fn: () => Promise | T, waitSeconds: number): Promise { 42 | return new Promise((resolve, reject) => { 43 | setTimeout(async () => { 44 | try { 45 | const val = await fn(); 46 | resolve(val); 47 | } catch (err) { 48 | reject(err); 49 | } 50 | }, waitSeconds * 1_000); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/shared/yagna/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./payment-api-adapter"; 2 | export * from "./market-api-adapter"; 3 | -------------------------------------------------------------------------------- /src/shared/yagna/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./adapters"; 2 | export * from "./repository"; 3 | export * from "./yagnaApi"; 4 | export * from "./event-reader"; 5 | -------------------------------------------------------------------------------- /src/shared/yagna/repository/activity-repository.ts: -------------------------------------------------------------------------------- 1 | import { Activity, ActivityStateEnum, IActivityRepository } from "../../../activity/activity"; 2 | import { ActivityApi } from "ya-ts-client"; 3 | import { IAgreementRepository } from "../../../market/agreement/agreement"; 4 | import { getMessageFromApiError } from "../../utils/apiErrorMessage"; 5 | import { GolemWorkError, WorkErrorCode } from "../../../activity"; 6 | import { CacheService } from "../../cache/CacheService"; 7 | 8 | export class ActivityRepository implements IActivityRepository { 9 | private stateCache: CacheService = new CacheService(); 10 | 11 | constructor( 12 | private readonly state: ActivityApi.RequestorStateService, 13 | private readonly agreementRepo: IAgreementRepository, 14 | ) {} 15 | 16 | async getById(id: string): Promise { 17 | try { 18 | const agreementId = await this.state.getActivityAgreement(id); 19 | const agreement = await this.agreementRepo.getById(agreementId); 20 | const previousState = this.stateCache.get(id) ?? ActivityStateEnum.New; 21 | const state = await this.getStateOfActivity(id); 22 | const usage = await this.state.getActivityUsage(id); 23 | 24 | return new Activity(id, agreement, state ?? ActivityStateEnum.Unknown, previousState, usage); 25 | } catch (error) { 26 | const message = getMessageFromApiError(error); 27 | throw new GolemWorkError( 28 | `Failed to get activity: ${message}`, 29 | WorkErrorCode.ActivityStatusQueryFailed, 30 | undefined, 31 | undefined, 32 | undefined, 33 | error, 34 | ); 35 | } 36 | } 37 | 38 | async getStateOfActivity(id: string): Promise { 39 | try { 40 | const yagnaStateResponse = await this.state.getActivityState(id); 41 | if (!yagnaStateResponse || yagnaStateResponse.state[0] === null) { 42 | return ActivityStateEnum.Unknown; 43 | } 44 | 45 | const state = ActivityStateEnum[yagnaStateResponse.state[0]]; 46 | this.stateCache.set(id, state); 47 | return state; 48 | } catch (error) { 49 | const message = getMessageFromApiError(error); 50 | throw new GolemWorkError( 51 | `Failed to get activity state: ${message}`, 52 | WorkErrorCode.ActivityStatusQueryFailed, 53 | undefined, 54 | undefined, 55 | undefined, 56 | error, 57 | ); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/shared/yagna/repository/agreement-repository.ts: -------------------------------------------------------------------------------- 1 | import { Agreement, IAgreementRepository } from "../../../market/agreement/agreement"; 2 | import { MarketApi } from "ya-ts-client"; 3 | import { GolemInternalError } from "../../error/golem-error"; 4 | import { IDemandRepository } from "../../../market/demand/demand"; 5 | import { getMessageFromApiError } from "../../utils/apiErrorMessage"; 6 | import { GolemMarketError, MarketErrorCode } from "../../../market"; 7 | 8 | export class AgreementRepository implements IAgreementRepository { 9 | constructor( 10 | private readonly api: MarketApi.RequestorService, 11 | private readonly demandRepo: IDemandRepository, 12 | ) {} 13 | 14 | async getById(id: string): Promise { 15 | let dto; 16 | try { 17 | dto = await this.api.getAgreement(id); 18 | } catch (error) { 19 | const message = getMessageFromApiError(error); 20 | throw new GolemMarketError(`Failed to get agreement: ${message}`, MarketErrorCode.CouldNotGetAgreement, error); 21 | } 22 | const { demandId } = dto.demand; 23 | const demand = this.demandRepo.getById(demandId); 24 | 25 | if (!demand) { 26 | throw new GolemInternalError(`Could not find information for demand ${demandId} of agreement ${id}`); 27 | } 28 | const agreement = new Agreement(id, dto, demand); 29 | return agreement; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/shared/yagna/repository/debit-note-repository.ts: -------------------------------------------------------------------------------- 1 | import { DebitNote, IDebitNoteRepository } from "../../../payment/debit_note"; 2 | import { MarketApi, PaymentApi } from "ya-ts-client"; 3 | import { getMessageFromApiError } from "../../utils/apiErrorMessage"; 4 | import { GolemPaymentError, PaymentErrorCode } from "../../../payment"; 5 | import { GolemMarketError, MarketErrorCode } from "../../../market"; 6 | 7 | export class DebitNoteRepository implements IDebitNoteRepository { 8 | constructor( 9 | private readonly paymentClient: PaymentApi.RequestorService, 10 | private readonly marketClient: MarketApi.RequestorService, 11 | ) {} 12 | 13 | async getById(id: string): Promise { 14 | let model; 15 | let agreement; 16 | try { 17 | model = await this.paymentClient.getDebitNote(id); 18 | } catch (error) { 19 | const message = getMessageFromApiError(error); 20 | throw new GolemPaymentError( 21 | `Failed to get debit note: ${message}`, 22 | PaymentErrorCode.CouldNotGetDebitNote, 23 | undefined, 24 | undefined, 25 | error, 26 | ); 27 | } 28 | 29 | try { 30 | agreement = await this.marketClient.getAgreement(model.agreementId); 31 | } catch (error) { 32 | const message = getMessageFromApiError(error); 33 | throw new GolemMarketError( 34 | `Failed to get agreement for debit note: ${message}`, 35 | MarketErrorCode.CouldNotGetAgreement, 36 | error, 37 | ); 38 | } 39 | 40 | const providerInfo = { 41 | id: model.issuerId, 42 | walletAddress: model.payeeAddr, 43 | name: agreement.offer.properties["golem.node.id.name"], 44 | }; 45 | 46 | return new DebitNote(model, providerInfo); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/shared/yagna/repository/demand-repository.ts: -------------------------------------------------------------------------------- 1 | import { Demand, IDemandRepository } from "../../../market/demand/demand"; 2 | import { MarketApi } from "ya-ts-client"; 3 | import { CacheService } from "../../cache/CacheService"; 4 | 5 | export class DemandRepository implements IDemandRepository { 6 | constructor( 7 | private readonly api: MarketApi.RequestorService, 8 | private readonly cache: CacheService, 9 | ) {} 10 | 11 | getById(id: string): Demand | undefined { 12 | return this.cache.get(id); 13 | } 14 | 15 | add(demand: Demand): Demand { 16 | this.cache.set(demand.id, demand); 17 | return demand; 18 | } 19 | 20 | getAll(): Demand[] { 21 | return this.cache.getAll(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/yagna/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./invoice-repository"; 2 | export * from "./debit-note-repository"; 3 | -------------------------------------------------------------------------------- /src/shared/yagna/repository/invoice-repository.ts: -------------------------------------------------------------------------------- 1 | import { IInvoiceRepository, Invoice } from "../../../payment/invoice"; 2 | import { MarketApi, PaymentApi } from "ya-ts-client"; 3 | import { getMessageFromApiError } from "../../utils/apiErrorMessage"; 4 | import { GolemPaymentError, PaymentErrorCode } from "../../../payment"; 5 | import { GolemMarketError, MarketErrorCode } from "../../../market"; 6 | 7 | export class InvoiceRepository implements IInvoiceRepository { 8 | constructor( 9 | private readonly paymentClient: PaymentApi.RequestorService, 10 | private readonly marketClient: MarketApi.RequestorService, 11 | ) {} 12 | 13 | async getById(id: string): Promise { 14 | let model; 15 | let agreement; 16 | try { 17 | model = await this.paymentClient.getInvoice(id); 18 | } catch (error) { 19 | const message = getMessageFromApiError(error); 20 | throw new GolemPaymentError( 21 | `Failed to get debit note: ${message}`, 22 | PaymentErrorCode.CouldNotGetInvoice, 23 | undefined, 24 | undefined, 25 | error, 26 | ); 27 | } 28 | 29 | try { 30 | agreement = await this.marketClient.getAgreement(model.agreementId); 31 | } catch (error) { 32 | const message = getMessageFromApiError(error); 33 | throw new GolemMarketError( 34 | `Failed to get agreement for invoice: ${message}`, 35 | MarketErrorCode.CouldNotGetAgreement, 36 | error, 37 | ); 38 | } 39 | const providerInfo = { 40 | id: model.issuerId, 41 | walletAddress: model.payeeAddr, 42 | name: agreement.offer.properties["golem.node.id.name"], 43 | }; 44 | 45 | return new Invoice(model, providerInfo); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/shared/yagna/repository/proposal-repository.ts: -------------------------------------------------------------------------------- 1 | import { OfferProposal } from "../../../market/proposal/offer-proposal"; 2 | import { MarketApi, IdentityApi } from "ya-ts-client"; 3 | import { Demand, GolemMarketError, MarketErrorCode } from "../../../market"; 4 | import { CacheService } from "../../cache/CacheService"; 5 | import { IProposalRepository, MarketProposal } from "../../../market/proposal/market-proposal"; 6 | import { OfferCounterProposal } from "../../../market/proposal/offer-counter-proposal"; 7 | 8 | export class ProposalRepository implements IProposalRepository { 9 | constructor( 10 | private readonly marketService: MarketApi.RequestorService, 11 | private readonly identityService: IdentityApi.DefaultService, 12 | private readonly cache: CacheService, 13 | ) {} 14 | 15 | add(proposal: MarketProposal) { 16 | this.cache.set(proposal.id, proposal); 17 | return proposal; 18 | } 19 | 20 | getById(id: string) { 21 | return this.cache.get(id); 22 | } 23 | 24 | async getByDemandAndId(demand: Demand, id: string): Promise { 25 | try { 26 | const dto = await this.marketService.getProposalOffer(demand.id, id); 27 | const identity = await this.identityService.getIdentity(); 28 | const isIssuedByRequestor = identity.identity === dto.issuerId ? "Requestor" : "Provider"; 29 | 30 | return isIssuedByRequestor ? new OfferCounterProposal(dto) : new OfferProposal(dto, demand); 31 | } catch (error) { 32 | const message = error.message; 33 | throw new GolemMarketError(`Failed to get proposal: ${message}`, MarketErrorCode.CouldNotGetProposal, error); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | -------------------------------------------------------------------------------- /tests/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // // *********************************************************** 2 | // // This example support/e2e.ts is processed and 3 | // // loaded automatically before your test files. 4 | // // 5 | // // This is a great place to put global configuration and 6 | // // behavior that modifies Cypress. 7 | // // 8 | // // You can change the location of this file or turn off 9 | // // automatically serving support files with the 10 | // // 'supportFile' configuration option. 11 | // // 12 | // // You can read more here: 13 | // // https://on.cypress.io/configuration 14 | // // *********************************************************** 15 | // 16 | // // Import commands.js using ES2015 syntax: 17 | // import "./commands"; 18 | 19 | beforeEach(() => { 20 | cy.intercept("GET", "https://unpkg.com/@golem-sdk/golem-js", (req) => { 21 | req.url = "http://localhost:3000/dist/golem-js.min.js"; 22 | req.continue(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "esModuleInterop": true, 6 | "types": ["cypress", "node"] 7 | }, 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tests/cypress/ui/hello-world.cy.ts: -------------------------------------------------------------------------------- 1 | describe("Test TaskExecutor API", () => { 2 | it("should run hello world example", () => { 3 | cy.visit("/hello"); 4 | cy.get("#YAGNA_APPKEY").clear().type(Cypress.env("YAGNA_APPKEY")); 5 | cy.get("#YAGNA_API_BASEPATH").clear().type(Cypress.env("YAGNA_API_BASEPATH")); 6 | cy.get("#SUBNET_TAG").clear().type(Cypress.env("YAGNA_SUBNET")); 7 | cy.get("#PAYMENT_NETWORK").clear().type(Cypress.env("PAYMENT_NETWORK")); 8 | cy.get("#echo").click(); 9 | cy.get("#results").should("include.text", "Hello Golem", { timeout: 60000 }); 10 | cy.get("#results").should("include.text", "Finalized renting process", { timeout: 10000 }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/cypress/ui/transfer-data.cy.ts: -------------------------------------------------------------------------------- 1 | describe("Transfer data example", () => { 2 | it("should run the example", () => { 3 | cy.visit("/transfer-data"); 4 | cy.get("#YAGNA_APPKEY").clear().type(Cypress.env("YAGNA_APPKEY")); 5 | cy.get("#YAGNA_API_BASEPATH").clear().type(Cypress.env("YAGNA_API_BASEPATH")); 6 | cy.get("#SUBNET_TAG").clear().type(Cypress.env("YAGNA_SUBNET")); 7 | cy.get("#PAYMENT_NETWORK").clear().type(Cypress.env("PAYMENT_NETWORK")); 8 | cy.get("#DATA").clear().type("Hello Golem!"); 9 | cy.get("#transfer-data").click(); 10 | cy.get("#results").should("include.text", "hELLO gOLEM!", { timeout: 60000 }); 11 | cy.get("#results").should("include.text", "Finalized renting process", { timeout: 10000 }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/docker/Provider.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION=22.04 2 | ARG YA_CORE_PROVIDER_VERSION=v0.15.2 3 | ARG YA_WASI_VERSION=v0.2.2 4 | ARG YA_VM_VERSION=v0.3.0 5 | 6 | FROM ubuntu:${UBUNTU_VERSION} 7 | ARG YA_CORE_PROVIDER_VERSION 8 | ARG YA_WASI_VERSION 9 | ARG YA_VM_VERSION 10 | ARG YA_DIR_INSTALLER=/ya-installer 11 | ARG YA_DIR_BIN=/usr/bin 12 | ARG YA_DIR_PLUGINS=/lib/yagna/plugins 13 | COPY /data-node/ya-provider/ /root/.local/share/ya-provider/ 14 | RUN apt-get update -q \ 15 | && apt-get install -q -y --no-install-recommends \ 16 | wget \ 17 | apt-transport-https \ 18 | ca-certificates \ 19 | xz-utils \ 20 | curl \ 21 | python3 \ 22 | && apt-get remove --purge -y \ 23 | && apt-get clean -y \ 24 | && rm -rf /var/lib/apt/lists/* \ 25 | && mkdir -p ${YA_DIR_PLUGINS} \ 26 | && mkdir ${YA_DIR_INSTALLER} \ 27 | && cd ${YA_DIR_INSTALLER} \ 28 | && wget -q "https://github.com/golemfactory/yagna/releases/download/${YA_CORE_PROVIDER_VERSION}/golem-provider-linux-${YA_CORE_PROVIDER_VERSION}.tar.gz" \ 29 | && wget -q "https://github.com/golemfactory/ya-runtime-wasi/releases/download/${YA_WASI_VERSION}/ya-runtime-wasi-linux-${YA_WASI_VERSION}.tar.gz" \ 30 | && wget -q "https://github.com/golemfactory/ya-runtime-vm/releases/download/${YA_VM_VERSION}/ya-runtime-vm-linux-${YA_VM_VERSION}.tar.gz" \ 31 | && tar -zxvf golem-provider-linux-${YA_CORE_PROVIDER_VERSION}.tar.gz \ 32 | && tar -zxvf ya-runtime-wasi-linux-${YA_WASI_VERSION}.tar.gz \ 33 | && tar -zxvf ya-runtime-vm-linux-${YA_VM_VERSION}.tar.gz \ 34 | && find golem-provider-linux-${YA_CORE_PROVIDER_VERSION} -executable -type f -exec cp {} ${YA_DIR_BIN} \; \ 35 | && cp -R golem-provider-linux-${YA_CORE_PROVIDER_VERSION}/plugins/* ${YA_DIR_PLUGINS} \ 36 | && cp -R ya-runtime-wasi-linux-${YA_WASI_VERSION}/* ${YA_DIR_PLUGINS} \ 37 | && cp -R ya-runtime-vm-linux-${YA_VM_VERSION}/* ${YA_DIR_PLUGINS} \ 38 | && rm -Rf ${YA_DIR_INSTALLER} 39 | COPY ./configureProvider.py /configureProvider.py 40 | 41 | CMD ["bash", "-c", "python3 /configureProvider.py && ya-provider rule set outbound everyone --mode whitelist && ya-provider whitelist add -p ipfs.io && golemsp run --payment-network testnet " ] 42 | -------------------------------------------------------------------------------- /tests/docker/Requestor.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UBUNTU_VERSION=22.04 2 | ARG YA_CORE_REQUESTOR_VERSION=v0.15.2 3 | 4 | FROM node:22 5 | ARG YA_CORE_REQUESTOR_VERSION 6 | ARG YA_DIR_INSTALLER=/ya-installer 7 | ARG YA_DIR_BIN=/usr/bin 8 | RUN apt-get update -q \ 9 | && apt-get install -q -y --no-install-recommends \ 10 | wget \ 11 | apt-transport-https \ 12 | ca-certificates \ 13 | xz-utils \ 14 | curl \ 15 | sshpass \ 16 | python3 \ 17 | libgtk2.0-0 \ 18 | libgtk-3-0 \ 19 | libgbm-dev \ 20 | libnotify-dev \ 21 | libgconf-2-4 \ 22 | libnss3 \ 23 | libxss1 \ 24 | libasound2 \ 25 | libxtst6 \ 26 | xauth \ 27 | xvfb \ 28 | chromium \ 29 | && apt-get remove --purge -y \ 30 | && apt-get clean -y \ 31 | && rm -rf /var/lib/apt/lists/* \ 32 | && mkdir ${YA_DIR_INSTALLER} \ 33 | && cd ${YA_DIR_INSTALLER} \ 34 | && wget -q "https://github.com/golemfactory/yagna/releases/download/${YA_CORE_REQUESTOR_VERSION}/golem-requestor-linux-${YA_CORE_REQUESTOR_VERSION}.tar.gz" \ 35 | && tar -zxvf golem-requestor-linux-${YA_CORE_REQUESTOR_VERSION}.tar.gz \ 36 | && find golem-requestor-linux-${YA_CORE_REQUESTOR_VERSION} -executable -type f -exec cp {} ${YA_DIR_BIN} \; \ 37 | && rm -Rf ${YA_DIR_INSTALLER} \ 38 | && wget -O ${YA_DIR_BIN}/websocat "https://github.com/vi/websocat/releases/download/v1.12.0/websocat_max.x86_64-unknown-linux-musl" \ 39 | && chmod +x ${YA_DIR_BIN}/websocat 40 | 41 | 42 | COPY ./startRequestor.sh /startRequestor.sh 43 | 44 | CMD ["bash", "-c", "/startRequestor.sh"] 45 | -------------------------------------------------------------------------------- /tests/docker/configureProvider.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | 5 | def update_globals(file_path, node_name): 6 | try: 7 | with open(file_path, 'r+') as f: 8 | data = json.load(f) 9 | data['node_name'] = node_name 10 | f.seek(0) 11 | print(data) 12 | json.dumps(data, f) 13 | f.truncate() 14 | print(f"Provider node name configured to {node_name}") 15 | except Exception as e: 16 | print(f"Error occurred: {str(e)}") 17 | 18 | 19 | update_globals(os.path.expanduser( 20 | '/root/.local/share/ya-provider/globals.json'), os.environ.get('NODE_NAME')) 21 | -------------------------------------------------------------------------------- /tests/docker/data-node/ya-provider/globals.json: -------------------------------------------------------------------------------- 1 | { 2 | "node_name": "node-name", 3 | "subnet": "public", 4 | "account": "0x797cE3Aa8dc255D13E48F31A6B23fe18b5924940" 5 | } 6 | -------------------------------------------------------------------------------- /tests/docker/data-node/ya-provider/hardware.json: -------------------------------------------------------------------------------- 1 | { 2 | "active": "default", 3 | "profiles": { 4 | "default": { 5 | "cpu_threads": 1, 6 | "mem_gib": 2.0, 7 | "storage_gib": 10.0 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/docker/data-node/ya-provider/presets.json: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "V1", 3 | "active": ["wasmtime", "vm"], 4 | "presets": [ 5 | { 6 | "name": "default", 7 | "exeunit-name": "wasmtime", 8 | "pricing-model": "linear", 9 | "initial-price": 0.0, 10 | "usage-coeffs": {} 11 | }, 12 | { 13 | "name": "vm", 14 | "exeunit-name": "vm", 15 | "pricing-model": "linear", 16 | "initial-price": 0.0, 17 | "usage-coeffs": { 18 | "golem.usage.cpu_sec": 0.0001, 19 | "golem.usage.duration_sec": 0.0 20 | } 21 | }, 22 | { 23 | "name": "wasmtime", 24 | "exeunit-name": "wasmtime", 25 | "pricing-model": "linear", 26 | "initial-price": 0.0, 27 | "usage-coeffs": { 28 | "golem.usage.cpu_sec": 0.0001, 29 | "golem.usage.duration_sec": 0.0 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tests/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | services: 3 | provider-1: 4 | build: 5 | context: . 6 | dockerfile: Provider.Dockerfile 7 | image: provider:latest 8 | restart: always 9 | deploy: 10 | replicas: 6 11 | volumes: 12 | - /etc/localtime:/etc/localtime:ro 13 | - /root/.local/share/yagna/ 14 | devices: 15 | - /dev/kvm:/dev/kvm 16 | healthcheck: 17 | test: ["CMD-SHELL", "curl -s -o /dev/null -w '%{http_code}' http://localhost:7465 | grep -q 401"] 18 | interval: 10s 19 | timeout: 5s 20 | retries: 1 21 | start_period: 40s 22 | environment: 23 | - MIN_AGREEMENT_EXPIRATION=30s 24 | - YA_NET_BROADCAST_SIZE=10 25 | - NODE_NAME=provider-1 26 | - SUBNET=${YAGNA_SUBNET:-golemjstest} 27 | - YA_NET_BIND_URL=udp://0.0.0.0:0 28 | - YA_NET_RELAY_HOST=63.34.24.27:7477 29 | provider-2: 30 | build: 31 | context: . 32 | dockerfile: Provider.Dockerfile 33 | image: provider:latest 34 | restart: always 35 | deploy: 36 | replicas: 6 37 | volumes: 38 | - /etc/localtime:/etc/localtime:ro 39 | - /root/.local/share/yagna/ 40 | devices: 41 | - /dev/kvm:/dev/kvm 42 | healthcheck: 43 | test: ["CMD-SHELL", "curl -s -o /dev/null -w '%{http_code}' http://localhost:7465 | grep -q 401"] 44 | interval: 10s 45 | timeout: 5s 46 | retries: 1 47 | start_period: 40s 48 | environment: 49 | - MIN_AGREEMENT_EXPIRATION=30s 50 | - YA_NET_BROADCAST_SIZE=10 51 | - NODE_NAME=provider-2 52 | - SUBNET=${YAGNA_SUBNET:-golemjstest} 53 | - YA_NET_BIND_URL=udp://0.0.0.0:0 54 | - YA_NET_RELAY_HOST=63.34.24.27:7477 55 | requestor: 56 | build: 57 | context: . 58 | dockerfile: Requestor.Dockerfile 59 | image: requestor:latest 60 | restart: always 61 | volumes: 62 | - /etc/localtime:/etc/localtime:ro 63 | - /root/.local/share/yagna/ 64 | - ../../:/golem-js 65 | environment: 66 | - YA_NET_BROADCAST_SIZE=10 67 | - YAGNA_AUTOCONF_APPKEY=try_golem 68 | - YAGNA_API_URL=http://0.0.0.0:7465 69 | - GSB_URL=tcp://0.0.0.0:7464 70 | - YAGNA_SUBNET=${YAGNA_SUBNET:-golemjstest} 71 | - YAGNA_APPKEY=try_golem 72 | - PAYMENT_NETWORK=${PAYMENT_NETWORK} 73 | - YA_NET_BIND_URL=udp://0.0.0.0:0 74 | - YA_NET_RELAY_HOST=63.34.24.27:7477 75 | healthcheck: 76 | test: ["CMD-SHELL", "curl -s -o /dev/null -w '%{http_code}' http://localhost:7465 | grep -q 401"] 77 | interval: 10s 78 | timeout: 5s 79 | retries: 1 80 | start_period: 40s 81 | -------------------------------------------------------------------------------- /tests/docker/fundRequestor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in {1..5}; do 4 | yagna payment fund --network ${PAYMENT_NETWORK} && yagna payment status --network ${PAYMENT_NETWORK} && exit 0 5 | done 6 | 7 | echo "yagna payment fund failed" >&2 8 | exit 1 -------------------------------------------------------------------------------- /tests/docker/startRequestor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | get_funds_from_faucet() { 4 | echo "Sending request to the faucet" 5 | yagna payment fund --network ${PAYMENT_NETWORK} 6 | } 7 | echo "Starting Yagna" 8 | yagna service run --api-allow-origin="*" 9 | -------------------------------------------------------------------------------- /tests/e2e/_setupEnv.ts: -------------------------------------------------------------------------------- 1 | process.env.DEBUG = "golem-js:*"; 2 | -------------------------------------------------------------------------------- /tests/e2e/_setupLogging.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper which allows replacing the default logger implemented in jest with the standard one for the duration of the test 3 | * 4 | * It was needed because of the way Goth tests are implemented - producing a lot of output on the console during the test 5 | * which is considered as an issue when using jest. Once we change the way we store Goth related logs, then we'll be 6 | * able to remove this file. 7 | */ 8 | import chalk from "chalk"; 9 | 10 | const jestConsole = console; 11 | 12 | beforeAll(async () => { 13 | global.console = await import("console"); 14 | }); 15 | 16 | afterAll(() => { 17 | global.console = jestConsole; 18 | }); 19 | 20 | beforeEach(() => { 21 | console.log(chalk.yellow(`\n\n---- Starting test: "${expect.getState().currentTestName}" ----\n\n`)); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/e2e/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "setupFiles": ["/_setupEnv.ts"], 4 | "setupFilesAfterEnv": ["/_setupLogging.ts"], 5 | "testTimeout": 180000, 6 | "extensionsToTreatAsEsm": [".ts"], 7 | "transform": { 8 | "^.+\\.tsx?$": [ 9 | "ts-jest", 10 | { 11 | "tsconfig": "tsconfig.spec.json", 12 | "useESM": true 13 | } 14 | ] 15 | }, 16 | "reporters": [ 17 | [ 18 | "jest-junit", 19 | { 20 | "outputDirectory": "reports", 21 | "outputName": "e2e-report.xml" 22 | } 23 | ], 24 | [ 25 | "github-actions", 26 | { 27 | "silent": false 28 | } 29 | ], 30 | "summary" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/examples/examples.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "cmd": "tsx", "path": "examples/rental-model/basic/many-of.ts" }, 3 | { "cmd": "tsx", "path": "examples/rental-model/basic/one-of.ts" }, 4 | { "cmd": "tsx", "path": "examples/rental-model/basic/vpn.ts" }, 5 | { "cmd": "tsx", "path": "examples/rental-model/basic/transfer.ts" }, 6 | { "cmd": "tsx", "path": "examples/rental-model/basic/run-and-stream.ts" }, 7 | { "cmd": "tsx", "path": "examples/rental-model/advanced/payment-filters.ts" }, 8 | { "cmd": "tsx", "path": "examples/rental-model/advanced/proposal-filter.ts" }, 9 | { "cmd": "tsx", "path": "examples/rental-model/advanced/proposal-predefined-filter.ts" }, 10 | { "cmd": "tsx", "path": "examples/rental-model/advanced/setup-and-teardown.ts" }, 11 | { "cmd": "tsx", "path": "examples/rental-model/advanced/reuse-allocation.ts" }, 12 | { "cmd": "tsx", "path": "examples/rental-model/advanced/local-image/local-image.ts" }, 13 | { "cmd": "tsx", "path": "examples/core-api/scan.ts" }, 14 | { "cmd": "tsx", "path": "examples/core-api/override-module.ts" }, 15 | { "cmd": "tsx", "path": "examples/core-api/manual-pools.ts" }, 16 | { "cmd": "tsx", "path": "examples/core-api/step-by-step.ts" }, 17 | { "cmd": "tsx", "path": "examples/experimental/deployment/new-api.ts" }, 18 | { "cmd": "tsx", "path": "examples/experimental/job/getJobById.ts" }, 19 | { "cmd": "tsx", "path": "examples/experimental/job/waitForResults.ts" }, 20 | { "cmd": "tsx", "path": "examples/experimental/job/cancel.ts" } 21 | ] 22 | -------------------------------------------------------------------------------- /tests/fixtures/alpine.gvmi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemfactory/golem-js/76e1342c318b1b62ae0a68fd9b3330999003d4a3/tests/fixtures/alpine.gvmi -------------------------------------------------------------------------------- /tests/fixtures/cubes.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemfactory/golem-js/76e1342c318b1b62ae0a68fd9b3330999003d4a3/tests/fixtures/cubes.blend -------------------------------------------------------------------------------- /tests/import/import.test.mjs: -------------------------------------------------------------------------------- 1 | describe("ESM Import", () => { 2 | test("Import @golem-sdk/golem-js", async () => { 3 | const { YagnaApi } = await import("@golem-sdk/golem-js"); 4 | expect(typeof YagnaApi).toBe("function"); 5 | }); 6 | 7 | test("Import @golem-sdk/golem-js/experimental", async () => { 8 | const { Job } = await import("@golem-sdk/golem-js/experimental"); 9 | expect(typeof Job).toBe("function"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/import/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | testEnvironment: "node", 4 | // disable transforming source files because we want to execute the 5 | // es modules directly 6 | transform: {}, 7 | moduleFileExtensions: ["js", "cjs", "mjs"], 8 | testMatch: ["**/?(*.)+(spec|test).mjs", "**/?(*.)+(spec|test).cjs"], 9 | }; 10 | -------------------------------------------------------------------------------- /tests/import/require.test.cjs: -------------------------------------------------------------------------------- 1 | describe("CommonJS Import", () => { 2 | test("Require @golem-sdk/golem-js", () => { 3 | const { YagnaApi } = require("@golem-sdk/golem-js"); 4 | expect(typeof YagnaApi).toBe("function"); 5 | }); 6 | 7 | test("Require @golem-sdk/golem-js/experimental", async () => { 8 | const { Job } = require("@golem-sdk/golem-js/experimental"); 9 | expect(typeof Job).toBe("function"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "roots": ["../src"], 5 | "testMatch": ["**/*.test.ts", "**/*.spec.ts"], 6 | "setupFilesAfterEnv": ["./jest.setup.ts"], 7 | "reporters": [ 8 | "default", 9 | [ 10 | "jest-junit", 11 | { 12 | "outputDirectory": "reports", 13 | "outputName": "unit-report.xml" 14 | } 15 | ], 16 | [ 17 | "github-actions", 18 | { 19 | "silent": false 20 | } 21 | ], 22 | "summary" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/jest.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace jest { 4 | interface Matchers { 5 | toMatchError(error: Error): R; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import { toMatchError } from "./utils/error_matcher"; 2 | expect.extend({ toMatchError }); 3 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["."], 4 | "exclude": ["./cypress"], 5 | "compilerOptions": { 6 | "types": ["jest"], 7 | "resolveJsonModule": true, 8 | "noImplicitAny": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/utils/error_matcher.ts: -------------------------------------------------------------------------------- 1 | type MatcherContext = jest.MatcherContext; 2 | type CustomMatcherResult = jest.CustomMatcherResult; 3 | 4 | function errorToObject(error: Error) { 5 | const { stack, ...publicFields } = error; 6 | 7 | return { 8 | class: error.constructor, 9 | message: error.message, 10 | publicFields, 11 | }; 12 | } 13 | 14 | type ErrorFactory = Error | (() => void); 15 | function unwrapErrorFactory(value: ErrorFactory): Error | undefined { 16 | if (typeof value === "function") { 17 | try { 18 | value(); 19 | } catch (error) { 20 | return error; 21 | } 22 | } else { 23 | return value; 24 | } 25 | } 26 | 27 | export function toMatchError( 28 | this: MatcherContext, 29 | gotErrorFactory: Error | (() => void), 30 | expectedError: Error, 31 | ): CustomMatcherResult { 32 | const gotError = unwrapErrorFactory(gotErrorFactory); 33 | const expected = errorToObject(expectedError); 34 | 35 | const diff: string[] = []; 36 | if (gotError === undefined) { 37 | diff.push("Expected to receive an error, but no error was thrown"); 38 | } else { 39 | const got = errorToObject(gotError); 40 | 41 | if (got.class !== expected.class) { 42 | diff.push("Error class is not the same:", this.utils.diff(expected.class, got.class) as string); 43 | } 44 | 45 | if (got.message !== expected.message) { 46 | diff.push("Error message is not the same:", this.utils.diff(expected.message, got.message) as string); 47 | } 48 | 49 | for (const key of Object.keys(expected.publicFields)) { 50 | if (expected.publicFields[key]?.constructor) { 51 | if (expected.publicFields[key].constructor.toString() !== got.publicFields?.[key]?.constructor.toString()) { 52 | diff.push( 53 | `Error field "${key}" class is not the same:`, 54 | this.utils.diff( 55 | expected.publicFields[key].constructor.toString(), 56 | got.publicFields?.[key]?.constructor.toString(), 57 | ) as string, 58 | ); 59 | } 60 | } 61 | if (!this.equals(expected.publicFields[key], got.publicFields?.[key])) { 62 | diff.push( 63 | `Error public field "${key}" are not the same:`, 64 | this.utils.diff(`${expected.publicFields[key]}`, `${got.publicFields?.[key]}`) as string, 65 | ); 66 | } 67 | } 68 | } 69 | 70 | return { 71 | pass: diff.length === 0, 72 | message: () => diff.join("\n"), 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /tests/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { map, of, throwError } from "rxjs"; 2 | import { Result, ResultData } from "../../src/activity/results"; 3 | 4 | /** 5 | * Helper function that makes it easy to prepare successful exec results creation 6 | */ 7 | export const buildExeScriptSuccessResult = (stdout: string): Result => 8 | new Result({ 9 | index: 0, 10 | eventDate: new Date().toISOString(), 11 | result: "Ok", 12 | stdout: stdout, 13 | stderr: "", 14 | message: "", 15 | isBatchFinished: true, 16 | }); 17 | 18 | /** 19 | * Helper function that makes preparing error exec results creation 20 | */ 21 | export const buildExeScriptErrorResult = (stderr: string, message: string, stdout = ""): Result => 22 | new Result({ 23 | index: 0, 24 | eventDate: new Date().toISOString(), 25 | result: "Error", 26 | stdout: stdout, 27 | stderr: stderr, 28 | message: message, 29 | isBatchFinished: true, 30 | }); 31 | 32 | /** 33 | * Use it to simulate responses from a "long-polled" API endpoint 34 | * 35 | * @param response The response to return after "polling time" 36 | * @param pollingTimeSec The time to wait before returning the response 37 | */ 38 | export const simulateLongPoll = (response: T, pollingTimeMs: number = 10) => 39 | new Promise((resolve) => { 40 | setTimeout(() => resolve(response), pollingTimeMs); 41 | }); 42 | 43 | /** 44 | * Helper function that makes preparing activity result returned by Activity.execute function 45 | */ 46 | export const buildExecutorResults = (successResults?: ResultData[], errorResults?: ResultData[], error?: Error) => { 47 | if (error) { 48 | return throwError(() => error); 49 | } 50 | return of(...(successResults ?? []), ...(errorResults ?? [])).pipe(map((resultData) => new Result(resultData))); 51 | }; 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "noImplicitAny": true, 5 | "target": "es2018", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "removeComments": false, 10 | "sourceMap": true, 11 | "noLib": false, 12 | "declaration": true, 13 | "useUnknownInCatchVariables": false, 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true, 16 | "lib": ["es2015", "es2016", "es2017", "es2018", "esnext", "dom"], 17 | "outDir": "dist", 18 | "typeRoots": ["node_modules/@types"] 19 | }, 20 | "typedocOptions": { 21 | "entryPoints": ["src"], 22 | "exclude": ["**/*.spec.ts", "**/*.test.ts"], 23 | "entryPointStrategy": "expand", 24 | "out": "docs", 25 | "sort": "static-first", 26 | "excludePrivate": true, 27 | "name": "Golem-JS API reference", 28 | "categorizeByGroup": false, 29 | "excludeExternals": true, 30 | "excludeInternal": true 31 | }, 32 | "include": ["src"], 33 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "tmp/"] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "noImplicitAny": false, 5 | "target": "es2018", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "removeComments": false, 10 | "sourceMap": true, 11 | "noLib": false, 12 | "declaration": true, 13 | "useUnknownInCatchVariables": false, 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true, 16 | "lib": ["es2015", "es2016", "es2017", "es2018", "esnext", "dom"], 17 | "outDir": "dist", 18 | "typeRoots": ["node_modules/@types"], 19 | "resolveJsonModule": true 20 | }, 21 | "exclude": ["dist", "node_modules", "examples", "cypress.config.ts"], 22 | "include": ["src/**/*.spec.ts", "tests/unit/**/*.test.ts", "tests/unit/**/*.spec.ts", "tests/jest.d.ts"], 23 | "ts-node": { 24 | "esm": true, 25 | "compilerOptions": { 26 | "module": "nodenext" 27 | } 28 | } 29 | } 30 | --------------------------------------------------------------------------------