├── src ├── balance │ ├── index.ts │ ├── types.ts │ ├── extrinsics.test.ts │ ├── storage.ts │ ├── storage.test.ts │ └── extrinsics.ts ├── tee │ ├── index.ts │ ├── storage.test.ts │ ├── types.ts │ ├── storage.ts │ └── extrinsics.ts ├── assets │ ├── index.ts │ ├── types.ts │ ├── storage.test.ts │ ├── storage.ts │ └── extrinsics.ts ├── auction │ ├── index.ts │ ├── types.ts │ ├── constants.test.ts │ ├── constants.ts │ ├── storage.test.ts │ ├── storage.ts │ └── extrinsics.test.ts ├── nft │ ├── index.ts │ ├── types.ts │ ├── constants.test.ts │ ├── constants.ts │ ├── storage.ts │ └── storage.test.ts ├── rent │ ├── index.ts │ ├── enum.ts │ ├── constants.test.ts │ ├── constants.ts │ ├── types.ts │ ├── utils.ts │ ├── storage.test.ts │ ├── storage.ts │ └── extrinsics.test.ts ├── marketplace │ ├── index.ts │ ├── enum.ts │ ├── constants.test.ts │ ├── constants.ts │ ├── types.ts │ ├── storage.test.ts │ ├── storage.ts │ ├── utils.ts │ └── extrinsics.test.ts ├── protocols │ ├── index.ts │ ├── enums.ts │ ├── constants.test.ts │ ├── constants.ts │ ├── types.ts │ ├── storage.ts │ ├── extrinsics.test.ts │ ├── storage.test.ts │ ├── utils.ts │ └── extrinsics.ts ├── helpers │ ├── index.ts │ ├── utils.test.ts │ ├── http.ts │ ├── crypto.ts │ ├── types.ts │ ├── utils.ts │ └── encryption.ts ├── _misc │ ├── CustomSequencer.js │ ├── scripts │ │ ├── test-setup.ts │ │ ├── test-teardown.ts │ │ └── build.mjs │ └── testingPairs.ts ├── account │ ├── index.test.ts │ └── index.ts ├── index.ts ├── blockchain │ ├── types.ts │ ├── utils.ts │ └── index.test.ts └── constants.ts ├── .prettierignore ├── .gitignore ├── docs ├── .nojekyll └── assets │ └── highlight.css ├── .prettierrc ├── examples ├── basic-usage │ ├── tsconfig.json │ ├── package.json │ └── README.md └── starter-project │ ├── tsconfig.json │ ├── package.json │ ├── src │ └── index.ts │ └── README.md ├── .eslintrc ├── jest.config.ts ├── .github ├── CODEOWNERS ├── workflows │ ├── pull-request.yml │ ├── test.yml │ └── ci.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── feature_request.yaml │ └── bug_report.yaml ├── tsconfig.json ├── Dockerfile ├── package.json └── CONTRIBUTING.md /src/balance/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./extrinsics" 2 | export * from "./storage" 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | .env* 4 | .vscode 5 | .DS_Store 6 | .npmrc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | .env* 4 | .vscode 5 | .DS_Store 6 | .npmrc 7 | 8 | *.js -------------------------------------------------------------------------------- /src/tee/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./storage" 2 | export * from "./extrinsics" 3 | export * from "./types" 4 | -------------------------------------------------------------------------------- /src/assets/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./storage" 2 | export * from "./extrinsics" 3 | export * from "./types" 4 | -------------------------------------------------------------------------------- /src/auction/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants" 2 | export * from "./extrinsics" 3 | export * from "./storage" 4 | export * from "./types" 5 | -------------------------------------------------------------------------------- /src/nft/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants" 2 | export * from "./extrinsics" 3 | export * from "./storage" 4 | export * from "./types" 5 | -------------------------------------------------------------------------------- /src/assets/types.ts: -------------------------------------------------------------------------------- 1 | export type AccountAssetDataType = { 2 | balance: string 3 | isFrozen: boolean 4 | reason: any 5 | extra: any 6 | } 7 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /src/rent/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./extrinsics" 2 | export * from "./storage" 3 | export * from "./constants" 4 | export * from "./types" 5 | export * from "./utils" 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": false, 5 | "arrowParens": "always", 6 | "semi": false, 7 | "tabWidth": 2 8 | } -------------------------------------------------------------------------------- /src/marketplace/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./extrinsics" 2 | export * from "./storage" 3 | export * from "./constants" 4 | export * from "./types" 5 | export * from "./utils" 6 | -------------------------------------------------------------------------------- /examples/basic-usage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18-strictest-esm/tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": ["src"], 5 | "exclude": ["node_modules"] 6 | } -------------------------------------------------------------------------------- /examples/starter-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18-strictest-esm/tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": ["src"], 5 | "exclude": ["node_modules"] 6 | } -------------------------------------------------------------------------------- /src/protocols/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants" 2 | export * from "./extrinsics" 3 | export * from "./storage" 4 | export * from "./types" 5 | export * from "./utils" 6 | export * from "./enums" 7 | -------------------------------------------------------------------------------- /src/balance/types.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | 3 | export type Balances = { 4 | free: BN 5 | reserved: BN 6 | frozen?: BN 7 | flags?: BN 8 | miscFrozen?: BN 9 | feeFrozen?: BN 10 | } 11 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./crypto" 2 | export * from "./encryption" 3 | export * from "./http" 4 | export * from "./ipfs" 5 | export * from "./nft" 6 | export * from "./tee" 7 | export * from "./utils" 8 | export * from "./types" 9 | -------------------------------------------------------------------------------- /src/tee/storage.test.ts: -------------------------------------------------------------------------------- 1 | describe("Testing Cluslter data", (): void => { 2 | it("TODO", async () => { 3 | expect(true).toBe(true) 4 | }) 5 | }) 6 | 7 | describe("Testing Enclave data", (): void => { 8 | it("TODO", async () => { 9 | expect(true).toBe(true) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/marketplace/enum.ts: -------------------------------------------------------------------------------- 1 | export enum MarketplaceKind { 2 | Public = "Public", 3 | Private = "Private", 4 | } 5 | 6 | export enum MarketplaceConfigAction { 7 | Noop = "Noop", 8 | Remove = "Remove", 9 | Set = "set", 10 | } 11 | 12 | export enum MarketplaceConfigFeeType { 13 | Percentage = "percentage", 14 | Flat = "flat", 15 | } 16 | -------------------------------------------------------------------------------- /src/protocols/enums.ts: -------------------------------------------------------------------------------- 1 | export enum TransmissionCancellationAction { 2 | UntilBlock = "untilBlock", 3 | Anytime = "anytime", 4 | None = "none", 5 | } 6 | 7 | export enum ProtocolAction { 8 | AtBlock = "atBlock", 9 | AtBlockWithReset = "atBlockWithReset", 10 | OnConsent = "onConsent", 11 | OnConsentAtBlock = "onConsentAtBlock", 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "eslint-config-prettier"], 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "jest/globals": true, 6 | "node": true 7 | }, 8 | "plugins": ["@typescript-eslint", "jest"], 9 | "rules": { 10 | "@typescript-eslint/no-explicit-any": "off" 11 | }, 12 | "ignorePatterns": ["build"] 13 | } 14 | -------------------------------------------------------------------------------- /examples/starter-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-project", 3 | "version": "1.0.0", 4 | "description": "You can use this project to bootstrap your own one.", 5 | "scripts": { 6 | "start": "tsc && node src/index.js" 7 | }, 8 | "dependencies": { 9 | "ternoa-js": "1.2.0-rc0" 10 | }, 11 | "devDependencies": { 12 | "@tsconfig/node18-strictest-esm": "1.0.0" 13 | }, 14 | "type": "module" 15 | } -------------------------------------------------------------------------------- /examples/basic-usage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-usage-example", 3 | "version": "1.0.0", 4 | "description": "Shows how to use the most common functionally from our SDK.", 5 | "scripts": { 6 | "start": "tsc && node src/index.js" 7 | }, 8 | "dependencies": { 9 | "ternoa-js": "1.2.0-rc0" 10 | }, 11 | "devDependencies": { 12 | "@tsconfig/node18-strictest-esm": "1.0.0" 13 | }, 14 | "type": "module" 15 | } -------------------------------------------------------------------------------- /src/_misc/CustomSequencer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const TestSequencer = require("@jest/test-sequencer").default 3 | 4 | class CustomSequencer extends TestSequencer { 5 | sort(tests) { 6 | return tests.sort((testA) => { 7 | if (testA.path.includes("balance")) { 8 | return 1 9 | } else { 10 | return -1 11 | } 12 | }) 13 | } 14 | } 15 | 16 | module.exports = CustomSequencer 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest" 2 | 3 | export default async (): Promise => { 4 | return { 5 | preset: "ts-jest", 6 | testEnvironment: "node", 7 | // testSequencer: "./src/_misc/CustomSequencer.js", 8 | detectOpenHandles: true, 9 | forceExit: true, 10 | testTimeout: 30000, 11 | silent: true, 12 | globalSetup: "./src/_misc/scripts/test-setup.ts", 13 | globalTeardown: "./src/_misc/scripts/test-teardown.ts", 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file defines the code owners of the repository 2 | 3 | # Code owners are automatically requested for review when someone opens a PR that modifies code that they own 4 | 5 | # Each line is a file pattern followed by one or more owners. Learn more here: 6 | 7 | # https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-code-owners 8 | 9 | # These owners will be the default owners for everything in 10 | 11 | # the repo. Unless a later match takes precedence, 12 | 13 | - @ipapandinas @Victor-Salomon 14 | -------------------------------------------------------------------------------- /src/helpers/utils.test.ts: -------------------------------------------------------------------------------- 1 | // import { Errors } from "../constants" 2 | import { formatPermill } from "./utils" 3 | 4 | describe("Testing formatPermill", (): void => { 5 | it("Should format royalty from a percent number to a permill", () => { 6 | const permillRoyalty = formatPermill(10) 7 | expect(permillRoyalty === 100000).toBe(true) 8 | }) 9 | // it("Should throw an error if royalty is not in range 0-100", async () => { 10 | // await expect(() => { 11 | // formatPermill(200) 12 | // }).rejects.toThrow(Error(Errors.ROYALTY_MUST_BE_PERCENTAGE)) 13 | // }) 14 | }) 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", // "node16", 5 | "lib": ["es2023"], // "dom", "dom.iterable" 6 | "declaration": true, 7 | "outDir": "build", 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "allowJs": true, 15 | "resolveJsonModule": true, 16 | }, 17 | "include": ["src"], 18 | "exclude": ["node_modules", "build", "**/*.test.ts"] // , "**/*.spec.ts", "src/_misc" 19 | } 20 | -------------------------------------------------------------------------------- /src/account/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mnemonicValidate } from "@polkadot/util-crypto" 2 | import { getKeyringFromSeed, generateSeed } from "./index" 3 | import { isValidAddress } from "../blockchain" 4 | 5 | test("Should generate a new seed", async () => { 6 | const seed = generateSeed() 7 | expect(mnemonicValidate(seed)).toBe(true) 8 | }) 9 | 10 | test("A valid seed should return a keypair", async () => { 11 | const seed = generateSeed() 12 | const keyring = await getKeyringFromSeed(seed) 13 | const address = keyring.address 14 | expect(isValidAddress(address)).toBe(true) 15 | expect(address).toBe(address) 16 | }) 17 | -------------------------------------------------------------------------------- /src/assets/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { getAccountAssetBalance } from "./storage" 2 | 3 | import { createTestPairs } from "../_misc/testingPairs" 4 | import { initializeApi } from "../blockchain" 5 | 6 | beforeAll(async () => { 7 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 8 | await initializeApi(endpoint) 9 | }) 10 | 11 | describe("Testing getAccountAssetBalance", (): void => { 12 | xit("Should get an empty asset balance", async (): Promise => { 13 | const { test: testAccount } = await createTestPairs() 14 | const balance = await getAccountAssetBalance(0, testAccount.address) 15 | expect(balance).toBe(null) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/marketplace/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeApi } from "../blockchain" 2 | import { getMarketplaceOffchainDataLimit, getMarketplaceAccountSizeLimit } from "./constants" 3 | 4 | beforeAll(() => { 5 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 6 | return initializeApi(endpoint) 7 | }) 8 | 9 | it("Testing marketplace offchain data size limit to be 150", async () => { 10 | const actual = await getMarketplaceOffchainDataLimit() 11 | expect(actual).toBeDefined() 12 | }) 13 | 14 | it("Testing marketplace account size limit to be 100 000", async () => { 15 | const actual = await getMarketplaceAccountSizeLimit() 16 | expect(actual).toBeDefined() 17 | }) 18 | -------------------------------------------------------------------------------- /examples/starter-project/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { initializeApi, safeDisconnect } from "ternoa-js" 3 | 4 | async function main() { 5 | // This will initialize the internal SDK API. 6 | // 7 | // It's not strictly necessary but it's good practice to initialize the API as soon as possible. 8 | // If this call is omitted then the first SDK call will return an exception. 9 | // You can also specify the endpoint by passing the endpoint address as the first argument. 10 | console.log("Connecting..."); 11 | await initializeApi(); 12 | 13 | console.log("Disconnecting..."); 14 | await safeDisconnect(); 15 | 16 | console.log("Done :)"); 17 | process.exit() 18 | } 19 | 20 | main() 21 | -------------------------------------------------------------------------------- /src/rent/enum.ts: -------------------------------------------------------------------------------- 1 | export enum DurationAction { 2 | Fixed = "fixed", 3 | Subscription = "subscription", 4 | } 5 | 6 | export enum SubscriptionActionDetails { 7 | PeriodLength = "periodLength", 8 | MaxDuration = "maxDuration", 9 | IsChangeable = "isChangeable", 10 | NewTerms = "newTerms", 11 | } 12 | 13 | export enum AcceptanceAction { 14 | AutoAcceptance = "autoAcceptance", 15 | ManualAcceptance = "manualAcceptance", 16 | } 17 | 18 | export enum RentFeeAction { 19 | Tokens = "tokens", 20 | NFT = "nft", 21 | } 22 | 23 | export enum CancellationFeeAction { 24 | FixedTokens = "fixedTokens", 25 | FlexibleTokens = "flexibleTokens", 26 | NFT = "nft", 27 | None = "None", 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: CI_DEV_PR 2 | on: 3 | pull_request: 4 | branches: 5 | - dev 6 | 7 | jobs: 8 | push: 9 | strategy: 10 | matrix: 11 | node-version: [16.x] 12 | 13 | name: PR 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - run: npm ci --ignore-scripts 24 | - run: npm run lint && npm run format && npm run build 25 | 26 | - name: Commit changes 27 | uses: stefanzweifel/git-auto-commit-action@v4 28 | with: 29 | commit_message: "Dev PR processed" 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: TEST 2 | on: 3 | pull_request: 4 | branches: 5 | - dev 6 | - next 7 | - main 8 | paths: 9 | - src/** 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [14.x] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci --ignore-scripts 24 | - name: Unit Tests 25 | run: | 26 | BLOCKCHAIN_ENDPOINT=${{secrets.BLOCKCHAIN_ENDPOINT}} SEED_TEST_FUNDS=${{secrets.SEED_TEST_FUNDS}} SEED_TEST_FUNDS_PUBLIC=${{secrets.SEED_TEST_FUNDS_PUBLIC}} npm run test 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | ci: 9 | strategy: 10 | matrix: 11 | step: ["lint", "format", "docs"] 12 | node-version: [14.x] 13 | name: ${{ matrix.step }} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: ${{ matrix.step }} 22 | if: always() 23 | run: | 24 | npm ci --ignore-scripts 25 | npm run ${{ matrix.step }} 26 | - name: Commit changes 27 | uses: stefanzweifel/git-auto-commit-action@v4 28 | with: 29 | commit_message: "PR: ${{ matrix.step }} fixed" 30 | -------------------------------------------------------------------------------- /src/auction/types.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | 3 | export type Bidder = { 4 | bidder: string 5 | amount: string 6 | amountRounded: number 7 | } 8 | 9 | export type AuctionChainRawDataType = { 10 | creator: string 11 | startBlock: number 12 | endBlock: number 13 | startPrice: BN 14 | buyItPrice: BN | null 15 | bidders: { list: [string[]] } 16 | marketplaceId: number 17 | isExtended: boolean 18 | } 19 | 20 | export type AuctionDataType = { 21 | creator: string 22 | startBlock: number 23 | endBlock: number 24 | startPrice: string 25 | startPriceRounded: number 26 | buyItPrice: string | null 27 | buyItPriceRounded: number | null 28 | bidders: Bidder[] 29 | marketplaceId: number 30 | isExtended: boolean 31 | } 32 | 33 | export type ClaimableBidBalanceDataType = { 34 | claimable: string 35 | claimableRounded: number 36 | } 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Types of changes 15 | 16 | 17 | 18 | - [ ] Bug fix (non-breaking change which fixes an issue) 19 | - [ ] New feature (non-breaking change which adds functionality) 20 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 21 | -------------------------------------------------------------------------------- /src/marketplace/constants.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | 3 | import { consts } from "../blockchain" 4 | import { txPallets, chainConstants } from "../constants" 5 | 6 | /** 7 | * @name getMarketplaceOffchainDataLimit 8 | * @summary Provides the maximum offchain data length. 9 | * @returns Number. 10 | */ 11 | export const getMarketplaceOffchainDataLimit = async (): Promise => { 12 | const limit = consts(txPallets.marketplace, chainConstants.offchainDataLimit) 13 | return (limit as any as BN).toNumber() 14 | } 15 | 16 | /** 17 | * @name getMarketplaceAccountSizeLimit 18 | * @summary The maximum number of accounts that can be stored inside the account list. 19 | * @returns Number. 20 | */ 21 | export const getMarketplaceAccountSizeLimit = async (): Promise => { 22 | const limit = consts(txPallets.marketplace, chainConstants.accountSizeLimit) 23 | return (limit as any as BN).toNumber() 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers/http.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios" 2 | 3 | export class HttpClient { 4 | client: AxiosInstance 5 | 6 | constructor(baseURL: string, timeout?: number) { 7 | this.client = axios.create({ 8 | baseURL, 9 | ...(timeout && { timeout }), 10 | }) 11 | } 12 | 13 | get = async (url: string, config = {}) => { 14 | const response = await this.client.get(url, config).catch((err) => { 15 | throw new Error(err) 16 | }) 17 | return response.data 18 | } 19 | 20 | getRaw = async (url: string, config = {}) => { 21 | const response = await this.client.get(url, config).catch((err) => { 22 | throw new Error(err) 23 | }) 24 | const { data, status } = response 25 | return { ...data, status } 26 | } 27 | 28 | post = async (url: string, data: any, config = {}) => { 29 | const response = await this.client.post(url, data, config).catch((err) => { 30 | throw new Error(err) 31 | }) 32 | return response.data 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/_misc/scripts/test-setup.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | import dotenv from "dotenv" 3 | import { getKeyringFromSeed } from "../../account" 4 | import { balancesTransferTx } from "../../balance" 5 | import { batchAllTxHex, initializeApi, submitTxBlocking } from "../../blockchain" 6 | import { Errors, WaitUntil } from "../../constants" 7 | import { PAIRSSR25519 } from "../testingPairs" 8 | 9 | dotenv.config() 10 | 11 | module.exports = async (): Promise => { 12 | if (!process.env.SEED_TEST_FUNDS) throw new Error(Errors.SEED_NOT_FOUND) 13 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 14 | 15 | await initializeApi(endpoint) 16 | const keyring = await getKeyringFromSeed(process.env.SEED_TEST_FUNDS) 17 | const pairs = PAIRSSR25519 18 | 19 | const amount = new BN("10000000000000000000000") 20 | const txs = await Promise.all(pairs.map((pair) => balancesTransferTx(pair.publicKey, amount))) 21 | const batchTx = await batchAllTxHex(txs) 22 | await submitTxBlocking(batchTx, WaitUntil.BlockInclusion, keyring) 23 | } 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account" 2 | export * from "./assets" 3 | export * from "./auction" 4 | export * from "./balance" 5 | export * from "./blockchain" 6 | export * from "./helpers" 7 | export * from "./nft" 8 | export * from "./rent" 9 | export * from "./tee" 10 | export * from "./marketplace" 11 | export * from "./protocols" 12 | export * from "./events" 13 | export * from "./constants" 14 | 15 | export * as Account from "./account" 16 | export * as Assets from "./assets" 17 | export * as Auction from "./auction" 18 | export * as Balance from "./balance" 19 | export * as Blockchain from "./blockchain" 20 | export * as TernoaHelpers from "./helpers" 21 | export * as Nft from "./nft" 22 | export * as Rent from "./rent" 23 | export * as Tee from "./tee" 24 | export * as Marketplace from "./marketplace" 25 | export * as Protocols from "./protocols" 26 | export * as TernoaEvents from "./events" 27 | export * as TernoaConstants from "./constants" 28 | 29 | export { hexToString, hexToU8a, stringToHex, u8aToHex } from "@polkadot/util" 30 | export { Blob, File, FormData } from "formdata-node" 31 | -------------------------------------------------------------------------------- /src/protocols/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeApi } from "../blockchain" 2 | import { 3 | getProtocolsActionsInBlockLimit, 4 | getSimultaneousTransmissionLimit, 5 | getMaxConsentListSize, 6 | getMaxBlockDuration, 7 | } from "./constants" 8 | 9 | beforeAll(() => { 10 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 11 | return initializeApi(endpoint) 12 | }) 13 | 14 | it("Maximum number of actions in one block should be defined", () => { 15 | const actual = getProtocolsActionsInBlockLimit() 16 | expect(actual).toBeDefined() 17 | }) 18 | 19 | it("Maximum number of simultaneous transmission protocol should be defined", () => { 20 | const actual = getSimultaneousTransmissionLimit() 21 | expect(actual).toBeDefined() 22 | }) 23 | 24 | it("Maximum size for the consent list should be defined", () => { 25 | const actual = getMaxConsentListSize() 26 | expect(actual).toBeDefined() 27 | }) 28 | 29 | it("Maximum block duration for a protocol should be defined", () => { 30 | const actual = getMaxBlockDuration() 31 | expect(actual).toBeDefined() 32 | }) 33 | -------------------------------------------------------------------------------- /src/account/index.ts: -------------------------------------------------------------------------------- 1 | import { mnemonicGenerate, cryptoWaitReady } from "@polkadot/util-crypto" 2 | import { Keyring } from "@polkadot/keyring" 3 | 4 | /** 5 | * @name generateSeed 6 | * @summary Generate a new seed 7 | * @returns The new seed 8 | */ 9 | export const generateSeed = mnemonicGenerate 10 | 11 | /** 12 | * @name getKeyringFromSeed 13 | * @summary Create a keyring from a seed 14 | * @param seed Mnemonic 15 | * @param hardPath Hard path derivation 16 | * @param softPath Soft path derivation 17 | * @param passwordPath Password path derivation 18 | * @returns A keyring pair 19 | */ 20 | export const getKeyringFromSeed = async (seed: string, hardPath?: string, softPath?: string, passwordPath?: string) => { 21 | await cryptoWaitReady() 22 | 23 | const _suri = 24 | seed + 25 | `${hardPath ? `//${hardPath}` : ""}` + 26 | `${softPath ? `/${softPath}` : ""}` + 27 | `${passwordPath ? `///${passwordPath}` : ""}` 28 | const keyring = new Keyring() 29 | return keyring.createFromUri(_suri, {}, "sr25519") 30 | } 31 | -------------------------------------------------------------------------------- /src/nft/types.ts: -------------------------------------------------------------------------------- 1 | export type NftState = { 2 | isCapsule: boolean 3 | isListed: boolean 4 | isSecret: boolean 5 | isDelegated: boolean 6 | isSoulbound: boolean 7 | isRented: boolean 8 | isSyncingSecret: boolean 9 | isSyncingCapsule: boolean 10 | isTransmission: boolean 11 | } 12 | 13 | export type NftData = { 14 | owner: string 15 | creator: string 16 | offchainData: string 17 | collectionId: number | undefined 18 | royalty: number 19 | state: NftState 20 | } 21 | 22 | export type SecretNftData = { 23 | nftId: number 24 | owner: string 25 | creator: string 26 | offchainData: string 27 | secretOffchainData: string 28 | collectionId: number | null 29 | royalty: number 30 | isSoulbound: boolean 31 | } 32 | 33 | export type CapsuleNFTData = Omit & { 34 | nftId: number 35 | capsuleOffchainData: string 36 | collectionId: number | null 37 | isSoulbound: boolean 38 | } 39 | 40 | export type CollectionData = { 41 | owner: string 42 | offchainData: string 43 | nfts: number[] 44 | limit: number 45 | isClosed: boolean 46 | } 47 | -------------------------------------------------------------------------------- /src/_misc/scripts/test-teardown.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | import dotenv from "dotenv" 3 | import { getKeyringFromSeed } from "../../account" 4 | import { getTransferrableBalance, balancesTransferAll } from "../../balance" 5 | import { safeDisconnect } from "../../blockchain" 6 | import { Errors, WaitUntil } from "../../constants" 7 | import { PAIRSSR25519 } from "../testingPairs" 8 | 9 | dotenv.config() 10 | 11 | module.exports = async (): Promise => { 12 | if (!process.env.SEED_TEST_FUNDS) throw new Error(Errors.SEED_NOT_FOUND) 13 | 14 | const dstKeyring = await getKeyringFromSeed(process.env.SEED_TEST_FUNDS) 15 | const pairs = PAIRSSR25519 16 | 17 | const zero = new BN("0") 18 | const keyrings = await Promise.all(pairs.map((pair) => getKeyringFromSeed(pair.seed))) 19 | const balances = await Promise.all(pairs.map((pair) => getTransferrableBalance(pair.publicKey))) 20 | const filteredKeyrings = keyrings.filter((_, i) => balances[i].gt(zero)) 21 | await Promise.all( 22 | filteredKeyrings.map((keyring) => 23 | balancesTransferAll(dstKeyring.address, false, keyring, WaitUntil.BlockInclusion), 24 | ), 25 | ) 26 | 27 | await safeDisconnect() 28 | } 29 | -------------------------------------------------------------------------------- /examples/basic-usage/README.md: -------------------------------------------------------------------------------- 1 | Table of Contents: 2 | 3 | - [Build And Run Locally](#build-and-run-locally) 4 | - [Build and Run With Podman](#build-and-run-with-podman) 5 | 6 | ## Build And Run Locally 7 | All the examples in this document assume that you use a Ubuntu like system. If that's not the case, you need to change the commands so that it works for your system. 8 | ```bash 9 | # Downloads the package lists and "updates" them. 10 | sudo apt update -y 11 | # Installing all dependencies 12 | sudo apt install git curl -y 13 | # Installing NVM. 14 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash 15 | # Starting a new bash environment so we have access to nvm command. 16 | exec bash 17 | # Installing Node and Typescript 18 | nvm install 18.7.0 && nvm use 18 && npm install -g typescript 19 | # This prints out the current node and typescript version. 20 | node -v && tsc -v 21 | # Compile starter-project 22 | tsc 23 | # You can run the project with calling node on index.js 24 | node ./src/index.js 25 | # or you can compile and run the project with the following command. 26 | npm run start 27 | ``` 28 | 29 | ## Build and Run With Podman 30 | Check the top level [Build And Run With Podman](../../README.md) documentation. -------------------------------------------------------------------------------- /examples/starter-project/README.md: -------------------------------------------------------------------------------- 1 | Table of Contents: 2 | 3 | - [Build And Run Locally](#build-and-run-locally) 4 | - [Build and Run With Podman](#build-and-run-with-podman) 5 | 6 | ## Build And Run Locally 7 | All the examples in this document assume that you use a Ubuntu like system. If that's not the case, you need to change the commands so that it works for your system. 8 | ```bash 9 | # Downloads the package lists and "updates" them. 10 | sudo apt update -y 11 | # Installing all dependencies 12 | sudo apt install git curl -y 13 | # Installing NVM. 14 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash 15 | # Starting a new bash environment so we have access to nvm command. 16 | exec bash 17 | # Installing Node and Typescript 18 | nvm install 18.7.0 && nvm use 18 && npm install -g typescript 19 | # This prints out the current node and typescript version. 20 | node -v && tsc -v 21 | # Compile starter-project 22 | tsc 23 | # You can run the project with calling node on index.js 24 | node ./src/index.js 25 | # or you can compile and run the project with the following command. 26 | npm run start 27 | ``` 28 | 29 | ## Build and Run With Podman 30 | Check the top level [Build And Run With Podman](../../README.md) documentation. -------------------------------------------------------------------------------- /src/nft/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeApi } from "../blockchain" 2 | import { 3 | getCollectionOffchainDataLimit, 4 | getCollectionSizeLimit, 5 | getInitialMintFee, 6 | getInitialSecretMintFee, 7 | getNftOffchainDataLimit, 8 | } from "./constants" 9 | 10 | beforeAll(() => { 11 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 12 | return initializeApi(endpoint) 13 | }) 14 | 15 | it("Testing initial mint fee to be 10 CAPS", async () => { 16 | const actual = await getInitialMintFee() 17 | expect(actual != undefined).toBe(true) 18 | }) 19 | 20 | it("Testing initial secret mint fee to be 75 CAPS", async () => { 21 | const actual = await getInitialSecretMintFee() 22 | expect(actual != undefined).toBe(true) 23 | }) 24 | 25 | it("Testing collection size limit to be 1 million", async () => { 26 | const actual = await getCollectionSizeLimit() 27 | expect(actual != undefined).toBe(true) 28 | }) 29 | 30 | it("Testing NFT offchain data size limit to be 150", async () => { 31 | const actual = await getNftOffchainDataLimit() 32 | expect(actual != undefined).toBe(true) 33 | }) 34 | 35 | it("Testing collection offchain data size limit to be 150", async () => { 36 | const actual = await getCollectionOffchainDataLimit() 37 | expect(actual != undefined).toBe(true) 38 | }) 39 | -------------------------------------------------------------------------------- /src/rent/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getAccountSizeLimit, 3 | getActionsInBlockLimit, 4 | getMaximumContractAvailabilityLimit, 5 | getMaximumContractDurationLimit, 6 | getSimultaneousContractLimit, 7 | } from "./constants" 8 | 9 | import { initializeApi } from "../blockchain" 10 | 11 | beforeAll(() => { 12 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 13 | return initializeApi(endpoint) 14 | }) 15 | 16 | it("Tests rent account size limit to be defined", () => { 17 | const actual = getAccountSizeLimit() 18 | expect(actual).toBeDefined() 19 | }) 20 | 21 | it("Tests the maximum rent actions in block to be defined", () => { 22 | const actual = getActionsInBlockLimit() 23 | expect(actual).toBeDefined() 24 | }) 25 | 26 | it("Tests the maximum of blocks during which a rent contract is available to be defined", () => { 27 | const actual = getMaximumContractAvailabilityLimit() 28 | expect(actual).toBeDefined() 29 | }) 30 | 31 | it("Tests the maximum of blocks that a contract can last for.", () => { 32 | const actual = getMaximumContractDurationLimit() 33 | expect(actual).toBeDefined() 34 | }) 35 | 36 | it("Tests the maximum number of simultaneous rent contract to be defined", () => { 37 | const actual = getSimultaneousContractLimit() 38 | expect(actual).toBeDefined() 39 | }) 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This is the first stage. Here we install all the dependencies that we need in order to build the Ternoa binary. 2 | FROM ubuntu:22.04 as builder 3 | 4 | ADD . ./workdir 5 | WORKDIR "/workdir" 6 | 7 | # Update system and install initial dependencies. 8 | RUN apt update -y && apt install git curl -y 9 | 10 | # Install NVM (Node Version Manager) 11 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash 12 | 13 | # Refreshes the shell session so that it can see nvm. Instead of executing this command you should just close the reopen your terminal. 14 | RUN . $HOME/.nvm/nvm.sh && \ 15 | # This tell nvm to install node version 18.7.0 and tell it to use it too. 16 | nvm install 18.7.0 && nvm use 18 && \ 17 | # Install Typescript. 18 | npm install -g typescript 19 | 20 | # Add Node and Tsc to PATH. This can be ignored. 21 | ENV PATH="${PATH}:/usr/bin/versions/node/v18.7.0/bin" 22 | 23 | # This prints out the current node and typescript version. 24 | RUN node -v && tsc -v 25 | 26 | # This install all project dependencies and builds the project. 27 | RUN cd ./examples/starter-project && npm i && tsc 28 | 29 | # Expose workdir so that it can be manipulated outside the container 30 | VOLUME ["/workdir"] 31 | 32 | # Run the starter project on start up 33 | CMD cd /workdir/examples/starter-project; tsc; node src/index.js -------------------------------------------------------------------------------- /src/tee/types.ts: -------------------------------------------------------------------------------- 1 | export type EnclaveDataType = { 2 | enclaveAddress: string 3 | apiUri: string 4 | } 5 | 6 | export type EnclaveHealthType = { 7 | status: number 8 | description: string 9 | enclave_address: string 10 | block_number: number 11 | sync_state: string 12 | version: string 13 | } 14 | 15 | export type PopulatedEnclavesDataType = { 16 | clusterId: number 17 | clusterType: "Disabled" | "Admin" | "Public" | "Private" 18 | enclaveAddress: string 19 | operatorAddress: string 20 | enclaveUrl: string 21 | enclaveSlot: number 22 | } 23 | 24 | export type EnclaveDataAndHealthType = PopulatedEnclavesDataType & { 25 | status: number 26 | blockNumber: number 27 | syncState: string 28 | description: string 29 | version?: string 30 | } 31 | 32 | export type ClusterDataType = { 33 | enclaves: [string, number][] 34 | clusterType: "Disabled" | "Admin" | "Public" | "Private" 35 | } 36 | 37 | export type NFTShareAvailableType = { 38 | enclave_id: string 39 | nft_id: number 40 | exists: boolean 41 | } 42 | 43 | export type ReportParamsType = { 44 | param1: number 45 | param2: number 46 | param3: number 47 | param4: number 48 | param5: number 49 | submittedBy: string 50 | } 51 | 52 | export type EnclaveQuoteRawType = { 53 | status: number 54 | data: string 55 | block_number?: number 56 | } 57 | 58 | export type EnclaveQuoteType = EnclaveQuoteRawType & PopulatedEnclavesDataType 59 | -------------------------------------------------------------------------------- /src/assets/storage.ts: -------------------------------------------------------------------------------- 1 | import { bnToBn } from "@polkadot/util" 2 | import BN from "bn.js" 3 | 4 | import { AccountAssetDataType } from "./types" 5 | 6 | import { chainQuery, txPallets } from "../constants" 7 | import { query } from "../blockchain" 8 | 9 | /** 10 | * @name getTotalAssetBalance 11 | * @summary The holdings of a specific account for a specific asset. 12 | * @param assetId The ID of the asset. 13 | * @param address Public address of the account to get balances. 14 | * @returns The holdings/balance information of the account : balance, isFrozen: boolean, reason, extra 15 | */ 16 | export const getAccountAssetData = async (assetId: number, address: string): Promise => { 17 | const accountData = ( 18 | await query(txPallets.assets, chainQuery.account, [assetId, address]) 19 | ).toJSON() as AccountAssetDataType 20 | return accountData 21 | } 22 | 23 | /** 24 | * @name getAssetBalance 25 | * @summary Get the balance of an account for a specific asset. 26 | * @param assetId The ID of the asset. 27 | * @param address Public address of the account to get balance. 28 | * @returns The balance of the account. 29 | */ 30 | export const getAccountAssetBalance = async (assetId: number, address: string): Promise => { 31 | const data = await getAccountAssetData(assetId, address) 32 | const balance = data ? bnToBn(data.balance) : null 33 | return balance 34 | } 35 | -------------------------------------------------------------------------------- /src/protocols/constants.ts: -------------------------------------------------------------------------------- 1 | import { consts } from "../blockchain" 2 | import { chainConstants, txPallets } from "../constants" 3 | 4 | /** 5 | * @name getProtocolsActionsInBlockLimit 6 | * @summary Maximum number of actions in one block. 7 | * @returns Number. 8 | */ 9 | export const getProtocolsActionsInBlockLimit = (): number => { 10 | const limit = consts(txPallets.transmissionProtocols, chainConstants.actionsInBlockLimit) 11 | return Number(limit.toString()) 12 | } 13 | 14 | /** 15 | * @name getSimultaneousTransmissionLimit 16 | * @summary Maximum number of simultaneous transmission protocol. 17 | * @returns Number. 18 | */ 19 | export const getSimultaneousTransmissionLimit = (): number => { 20 | const limit = consts(txPallets.transmissionProtocols, chainConstants.simultaneousTransmissionLimit) 21 | return Number(limit.toString()) 22 | } 23 | /** 24 | * @name getMaxConsentListSize 25 | * @summary Maximum size for the consent list. 26 | * @returns Number. 27 | */ 28 | export const getMaxConsentListSize = (): number => { 29 | const size = consts(txPallets.transmissionProtocols, chainConstants.maxConsentListSize) 30 | return Number(size.toString()) 31 | } 32 | 33 | /** 34 | * @name getMaxBlockDuration 35 | * @summary Maximum block duration for a protocol. 36 | * @returns Number. 37 | */ 38 | export const getMaxBlockDuration = (): number => { 39 | const block = consts(txPallets.transmissionProtocols, chainConstants.maxBlockDuration) 40 | return Number(block.toString()) 41 | } 42 | -------------------------------------------------------------------------------- /src/protocols/types.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolAction, TransmissionCancellationAction } from "./enums" // ProtocolAction, 2 | 3 | export type ProtocolAtBlockQueue = { 4 | nftId: number 5 | blockNumber: number 6 | } 7 | 8 | export type ProtocolOnConsentData = { 9 | consentList: string[] 10 | threshold: number 11 | block: number 12 | } 13 | 14 | export type TransmissionAtBlock = { [ProtocolAction.AtBlock]: number } 15 | export type TransmissionAtBlockWithReset = { [ProtocolAction.AtBlockWithReset]: number } 16 | export type TransmissionOnConsent = { 17 | [ProtocolAction.OnConsent]: Omit 18 | } 19 | export type TransmissionOnConsentAtBlock = { 20 | [ProtocolAction.OnConsentAtBlock]: ProtocolOnConsentData 21 | } 22 | export type Protocols = 23 | | TransmissionAtBlock 24 | | TransmissionAtBlockWithReset 25 | | TransmissionOnConsent 26 | | TransmissionOnConsentAtBlock 27 | 28 | export type TransmissionCancellationAtBlock = { [TransmissionCancellationAction.UntilBlock]: number } 29 | export type TransmissionCancellationAtAnytime = { [TransmissionCancellationAction.Anytime]: null } 30 | export type TransmissionCancellationAtNone = { [TransmissionCancellationAction.None]: null } 31 | export type TransmissionCancellation = 32 | | TransmissionCancellationAtNone 33 | | TransmissionCancellationAtAnytime 34 | | TransmissionCancellationAtBlock 35 | 36 | export type Transmissions = { 37 | recipient: string 38 | protocol: Protocols 39 | cancellation: TransmissionCancellation 40 | } 41 | -------------------------------------------------------------------------------- /src/blockchain/types.ts: -------------------------------------------------------------------------------- 1 | import { BatchInterruptedEvent, BlockchainEvents, ExtrinsicFailedEvent, ItemFailedEvent } from "../events" 2 | 3 | import { BlockInfo } from "./utils" 4 | 5 | export interface IFormatBalanceOptions { 6 | /** 7 | * @description The number of decimals. 8 | */ 9 | decimals?: number 10 | /** 11 | * @description Format the number with this specific unit. 12 | */ 13 | forceUnit?: string 14 | /** 15 | * @description Format with SI, i.e. m/M/etc. 16 | */ 17 | withSi?: boolean 18 | /** 19 | * @description Format with full SI, i.e. mili/Mega/etc. 20 | */ 21 | withSiFull?: boolean 22 | /** 23 | * @description Add the unit (useful in Balance formats). 24 | */ 25 | withUnit?: boolean | string 26 | /** 27 | * @description Token Unit. 28 | */ 29 | unit?: string 30 | } 31 | 32 | export type SubmitTxBlockingType = { 33 | blockInfo: BlockInfo 34 | events: BlockchainEvents 35 | txHash: `0x${string}` 36 | } 37 | 38 | export type TransactionHashType = `0x${string}` 39 | 40 | export type CheckTransactionType = { 41 | isTxSuccess: boolean 42 | events?: BlockchainEvents 43 | failedEvent?: ExtrinsicFailedEvent 44 | } 45 | export interface ICheckBatch extends CheckTransactionType { 46 | isBatchInterrupted: boolean 47 | indexInterrupted?: number 48 | batchInterruptedEvent?: BatchInterruptedEvent 49 | } 50 | 51 | export interface ICheckForceBatch extends CheckTransactionType { 52 | isBatchCompleteWithoutErrors?: boolean 53 | failedItems?: ItemFailedEvent[] 54 | } 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: Feature request 4 | labels: ['enhancement ─=≡Σ((( つ◕ل͜◕)つ'] 5 | body: 6 | - type: textarea 7 | id: feature_request_description 8 | attributes: 9 | label: Is your feature request related to a problem? Please describe. 10 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: feature_request_solution_description 15 | attributes: 16 | label: Describe the solution you'd like 17 | description: A clear and concise description of what you want to happen 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: feature_request_alternatives 22 | attributes: 23 | label: Describe alternatives you've considered 24 | description: A clear and concise description of any alternative solutions or features you've considered 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: feature_request_additional_context 29 | attributes: 30 | label: Additional context 31 | description: Add any other context or screenshots about the feature request here 32 | - type: markdown 33 | attributes: 34 | value: | 35 | **Want to contribute?** 36 | - type: markdown 37 | attributes: 38 | value: We love contributions from the Ternoa community! Please comment on an issue if you're interested in helping out with a PR. 39 | -------------------------------------------------------------------------------- /src/auction/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeApi } from "../blockchain" 2 | import { 3 | getAuctionEndingPeriod, 4 | getAuctionGracePeriod, 5 | getBidderListLengthLimit, 6 | getMaxAuctionDelay, 7 | getMinAuctionDuration, 8 | getMaxAuctionDuration, 9 | getParallelAuctionLimit, 10 | } from "./constants" 11 | 12 | beforeAll(() => { 13 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 14 | return initializeApi(endpoint) 15 | }) 16 | 17 | it("Testing to get the auction ending period constant", async () => { 18 | const endingPeriod = getAuctionEndingPeriod() 19 | expect(endingPeriod).toBeDefined() 20 | }) 21 | 22 | it("Testing to get the auction grace period constant", async () => { 23 | const gracePeriod = getAuctionGracePeriod() 24 | expect(gracePeriod).toBeDefined() 25 | }) 26 | 27 | it("Testing to get the bidder list lenggth limit constant", async () => { 28 | const limit = getBidderListLengthLimit() 29 | expect(limit).toBeDefined() 30 | }) 31 | 32 | it("Testing to get the auction maximum delay constant", async () => { 33 | const delay = getMaxAuctionDelay() 34 | expect(delay).toBeDefined() 35 | }) 36 | 37 | it("Testing to get the auction minimum duration constant", async () => { 38 | const duration = getMinAuctionDuration() 39 | expect(duration).toBeDefined() 40 | }) 41 | 42 | it("Testing to get the auction maximum duration constant", async () => { 43 | const duration = getMaxAuctionDuration() 44 | expect(duration).toBeDefined() 45 | }) 46 | 47 | it("Testing to get the parallel auction limit constant", async () => { 48 | const limit = getParallelAuctionLimit() 49 | expect(limit).toBeDefined() 50 | }) 51 | -------------------------------------------------------------------------------- /src/marketplace/types.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | 3 | import { MarketplaceConfigAction } from "./enum" 4 | 5 | type RequireOnlyOne = Pick> & 6 | { 7 | [K in Keys]-?: Required> & Partial, undefined>> 8 | }[Keys] 9 | 10 | export interface IFeeType { 11 | percentage: number 12 | flat: BN | number | string 13 | } 14 | 15 | export type SetFeeType = { [MarketplaceConfigAction.Set]: RequireOnlyOne } 16 | 17 | export type CommissionFeeType = MarketplaceConfigAction.Noop | MarketplaceConfigAction.Remove | SetFeeType 18 | export type ListingFeeType = MarketplaceConfigAction.Noop | MarketplaceConfigAction.Remove | SetFeeType 19 | export type AccountListType = 20 | | MarketplaceConfigAction.Noop 21 | | MarketplaceConfigAction.Remove 22 | | { [MarketplaceConfigAction.Set]: string[] } 23 | export type OffchainDataType = 24 | | MarketplaceConfigAction.Noop 25 | | MarketplaceConfigAction.Remove 26 | | { [MarketplaceConfigAction.Set]: string } 27 | export type CollectionListType = 28 | | MarketplaceConfigAction.Noop 29 | | MarketplaceConfigAction.Remove 30 | | { [MarketplaceConfigAction.Set]: number[] } 31 | 32 | export type MarketplaceDataType = { 33 | owner: string 34 | kind: string 35 | commissionFee: IFeeType | undefined 36 | listingFee: IFeeType | undefined 37 | accountList: string[] | undefined 38 | offchainData: string | undefined 39 | collectionList: number[] | undefined 40 | } 41 | 42 | export interface IListedNft { 43 | accountId: string 44 | marketplaceId: number 45 | price: BN 46 | commissionFee?: RequireOnlyOne 47 | } 48 | -------------------------------------------------------------------------------- /src/_misc/testingPairs.ts: -------------------------------------------------------------------------------- 1 | import { cryptoWaitReady } from "@polkadot/util-crypto" 2 | import { Keyring } from "@polkadot/keyring" 3 | import { KeypairType } from "@polkadot/util-crypto/types" 4 | import type { IKeyringPair } from "@polkadot/types/types" 5 | 6 | interface PairDef { 7 | name: string 8 | publicKey: string 9 | seed: string 10 | type: KeypairType 11 | } 12 | 13 | export interface TestKeyringMap { 14 | [index: string]: IKeyringPair 15 | } 16 | 17 | export const PAIRSSR25519: PairDef[] = [ 18 | { 19 | name: "test", 20 | publicKey: "5GesFQSwhmuMKAHcDrfm21Z5xrq6kW93C1ch2Xosq1rXx2Eh", 21 | seed: "soccer traffic version fault humor tackle bid tape obvious wild fish coin", 22 | type: "sr25519", 23 | }, 24 | { 25 | name: "dest", 26 | publicKey: "5C5U1zoKAytwirg2XD2cUDXrAShyQ4dyx5QkPf7ChWQAykLR", 27 | seed: "sponsor music pony breeze recall engage sport jelly certain unit spoil shift", 28 | type: "sr25519", 29 | }, 30 | ] 31 | 32 | /** 33 | * @name testKeyring 34 | * @summary Create keyring pairs with locked test accounts 35 | */ 36 | export const createTestPairs = async (): Promise => { 37 | await cryptoWaitReady() 38 | const keyring = new Keyring() 39 | 40 | for (const { name, seed, type } of PAIRSSR25519) { 41 | keyring.addPair( 42 | keyring.createFromUri( 43 | seed, 44 | { 45 | isTesting: true, 46 | name, 47 | }, 48 | type, 49 | ), 50 | ) 51 | } 52 | 53 | const pairs = keyring.getPairs() 54 | const map: TestKeyringMap = {} 55 | 56 | for (const p of pairs) { 57 | map[p.meta.name as string] = p 58 | } 59 | 60 | return map 61 | } 62 | -------------------------------------------------------------------------------- /src/balance/extrinsics.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeApi, numberToBalance } from "../blockchain" 2 | import { WaitUntil } from "../constants" 3 | import { createTestPairs } from "../_misc/testingPairs" 4 | import { balancesTransfer, balancesTransferKeepAlive } from "./extrinsics" 5 | 6 | beforeAll(async () => { 7 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 8 | await initializeApi(endpoint) 9 | }) 10 | 11 | describe("Testing balance transfers", (): void => { 12 | it("Transfer 1 CAPS from the testing account", async (): Promise => { 13 | const { test: testAccount, dest: destAccount } = await createTestPairs() 14 | const oneCapsAmount = numberToBalance(1) 15 | const event = await balancesTransfer(destAccount.address, oneCapsAmount, testAccount, WaitUntil.BlockInclusion) 16 | expect( 17 | event.from === testAccount.address && 18 | event.to === destAccount.address && 19 | event.amount === oneCapsAmount.toString() && 20 | event.amountRounded === 1, 21 | ).toBe(true) 22 | }) 23 | 24 | it("Transfer 1 CAPS from the testing account using balancesTransferKeepAlive", async (): Promise => { 25 | const { test: testAccount, dest: destAccount } = await createTestPairs() 26 | const oneCapsAmount = numberToBalance(1) 27 | const event = await balancesTransferKeepAlive( 28 | destAccount.address, 29 | oneCapsAmount, 30 | testAccount, 31 | WaitUntil.BlockInclusion, 32 | ) 33 | expect( 34 | event.from === testAccount.address && 35 | event.to === destAccount.address && 36 | event.amount === oneCapsAmount.toString() && 37 | event.amountRounded === 1, 38 | ).toBe(true) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/nft/constants.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | 3 | import { consts } from "../blockchain" 4 | import { chainConstants, txPallets } from "../constants" 5 | 6 | /** 7 | * @name getInitialMintFee 8 | * @summary Original mint fee. 9 | * @returns Original NFT mint fee. 10 | */ 11 | export const getInitialMintFee = async (): Promise => { 12 | const fee = consts(txPallets.nft, chainConstants.initialMintFee) 13 | return fee as any as BN 14 | } 15 | 16 | /** 17 | * @name getInitialSecretMintFee 18 | * @summary Original secret mint fee. 19 | * @returns Original Secret NFT mint fee. 20 | */ 21 | export const getInitialSecretMintFee = async (): Promise => { 22 | const fee = consts(txPallets.nft, chainConstants.initialSecretMintFee) 23 | return fee as any as BN 24 | } 25 | 26 | /** 27 | * @name getCollectionSizeLimit 28 | * @summary Maximum collection length. 29 | * @returns Number. 30 | */ 31 | export const getCollectionSizeLimit = async (): Promise => { 32 | const limit = consts(txPallets.nft, chainConstants.collectionSizeLimit) 33 | return (limit as any as BN).toNumber() 34 | } 35 | 36 | /** 37 | * @name getNftOffchainDataLimit 38 | * @summary Provides the maximum offchain data length. 39 | * @returns Number. 40 | */ 41 | export const getNftOffchainDataLimit = async (): Promise => { 42 | const limit = consts(txPallets.nft, chainConstants.nftOffchainDataLimit) 43 | return (limit as any as BN).toNumber() 44 | } 45 | 46 | /** 47 | * @name getCollectionOffchainDataLimit 48 | * @summary Provides the maximum offchain data length. 49 | * @returns Number. 50 | */ 51 | export const getCollectionOffchainDataLimit = async (): Promise => { 52 | const limit = consts(txPallets.nft, chainConstants.collectionOffchainDataLimit) 53 | return (limit as any as BN).toNumber() 54 | } 55 | -------------------------------------------------------------------------------- /src/rent/constants.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | 3 | import { chainConstants, txPallets } from "../constants" 4 | import { consts } from "../blockchain" 5 | 6 | /** 7 | * @name getAccountSizeLimit 8 | * @summary The maximum number of accounts that can be stored inside the account list of acceptance. 9 | * @returns Number. 10 | */ 11 | export const getAccountSizeLimit = (): number => { 12 | const limit = consts(txPallets.rent, chainConstants.accountSizeLimit) 13 | return (limit as any as BN).toNumber() 14 | } 15 | 16 | /** 17 | * @name getActionsInBlockLimit 18 | * @summary Maximum number of related automatic rent actions in block. 19 | * @returns Number. 20 | */ 21 | export const getActionsInBlockLimit = (): number => { 22 | const limit = consts(txPallets.rent, chainConstants.actionsInBlockLimit) 23 | return (limit as any as BN).toNumber() 24 | } 25 | 26 | /** 27 | * @name getMaximumContractAvailabilityLimit 28 | * @summary Maximum number of blocks during which a rent contract is available. 29 | * @returns Number. 30 | */ 31 | export const getMaximumContractAvailabilityLimit = (): number => { 32 | const limit = consts(txPallets.rent, chainConstants.maximumContractAvailabilityLimit) 33 | return (limit as any as BN).toNumber() 34 | } 35 | 36 | /** 37 | * @name getMaximumContractDurationLimit 38 | * @summary Maximum number of blocks that a contract can last for. 39 | * @returns Number. 40 | */ 41 | export const getMaximumContractDurationLimit = (): number => { 42 | const limit = consts(txPallets.rent, chainConstants.maximumContractDurationLimit) 43 | return (limit as any as BN).toNumber() 44 | } 45 | 46 | /** 47 | * @name getSimultaneousContractLimit 48 | * @summary Maximum number of simultaneous rent contract. 49 | * @returns Number. 50 | */ 51 | export const getSimultaneousContractLimit = (): number => { 52 | const limit = consts(txPallets.rent, chainConstants.simultaneousContractLimit) 53 | return (limit as any as BN).toNumber() 54 | } 55 | -------------------------------------------------------------------------------- /src/assets/extrinsics.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | import type { IKeyringPair } from "@polkadot/types/types" 3 | 4 | import { txActions, txPallets, WaitUntil } from "../constants" 5 | import { createTxHex, numberToBalance, submitTxBlocking, TransactionHashType } from "../blockchain" 6 | import { AssetTransferredEvent } from "../events" 7 | 8 | /** 9 | * @name assetTransferTx 10 | * @summary Creates an unsigned unsubmitted Assets-Transfer Transaction Hash. 11 | * @param id ID of the Asset 12 | * @param to Public address of the account to transfer the amount to. 13 | * @param amount Token amount to transfer. 14 | * @returns Unsigned unsubmitted Assets-Transfer Transaction Hash. The Hash is only valid for 5 minutes. 15 | */ 16 | export const assetTransferTx = async (id: number, to: string, amount: number | BN): Promise => { 17 | const formattedAmount = typeof amount === "number" ? numberToBalance(amount) : amount 18 | return await createTxHex(txPallets.assets, txActions.transfer, [id, to, formattedAmount]) 19 | } 20 | 21 | /** 22 | * @name assetTransfer 23 | * @summary Transfers some balance to another account. 24 | * @param id ID of the Asset 25 | * @param to Public address of the account to transfer the amount to. 26 | * @param amount Token amount to transfer. 27 | * @param keyring Account that will sign the transaction. 28 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 29 | * @returns AssetTransferredEvent Blockchain event. 30 | */ 31 | export const assetTransfer = async ( 32 | id: number, 33 | to: string, 34 | amount: number | BN, 35 | keyring: IKeyringPair, 36 | waitUntil: WaitUntil, 37 | ): Promise => { 38 | const tx = await assetTransferTx(id, to, amount) 39 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 40 | return events.findEventOrThrow(AssetTransferredEvent) 41 | } 42 | -------------------------------------------------------------------------------- /src/_misc/scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync, copyFileSync } from "fs" 2 | import { resolve, join, basename } from "path" 3 | 4 | const packagePath = process.cwd() 5 | const buildPath = join(packagePath, "./build") 6 | 7 | const writeJson = (targetPath, obj) => writeFileSync(targetPath, JSON.stringify(obj, null, 2), "utf8") 8 | 9 | async function createPackageFile() { 10 | const packageData = JSON.parse(readFileSync(resolve(packagePath, "./package.json"), "utf8")) 11 | const newPackageData = { 12 | ...packageData, 13 | main: "./index.js", 14 | types: "./index.d.ts", 15 | typesVersions: { 16 | "*": { 17 | account: ["./account/index.d.ts"], 18 | assets: ["./assets/index.d.ts"], 19 | auction: ["./auction/index.d.ts"], 20 | balance: ["./balance/index.d.ts"], 21 | blockchain: ["./blockchain/index.d.ts"], 22 | helpers: ["./helpers/index.d.ts"], 23 | nft: ["./nft/index.d.ts"], 24 | rent: ["./rent/index.d.ts"], 25 | tee: ["./tee/index.d.ts"], 26 | marketplace: ["./marketplace/index.d.ts"], 27 | protocols: ["./protocols/index.d.ts"], 28 | events: ["./events.d.ts"], 29 | constants: ["./constants.d.ts"], 30 | }, 31 | }, 32 | } 33 | 34 | delete newPackageData.scripts 35 | delete newPackageData.devDependencies 36 | 37 | const targetPath = resolve(buildPath, "./package.json") 38 | writeJson(targetPath, newPackageData) 39 | 40 | console.log(`Created package.json in ${targetPath}`) 41 | } 42 | 43 | async function includeFileInBuild(file) { 44 | const sourcePath = resolve(packagePath, file) 45 | const targetPath = resolve(buildPath, basename(file)) 46 | copyFileSync(sourcePath, targetPath) 47 | console.log(`Copied ${sourcePath} to ${targetPath}`) 48 | } 49 | 50 | async function run() { 51 | try { 52 | await createPackageFile() 53 | await includeFileInBuild("./README.md") 54 | await includeFileInBuild("./LICENSE") 55 | } catch (err) { 56 | console.error(err) 57 | process.exit(1) 58 | } 59 | } 60 | 61 | run() 62 | -------------------------------------------------------------------------------- /src/blockchain/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Block } from "@polkadot/types/interfaces/runtime" 2 | 3 | import { getRawApi } from "../blockchain" 4 | import { Errors } from "../constants" 5 | 6 | export function sleep(ms: number) { 7 | return new Promise((resolve) => setTimeout(resolve, ms)) 8 | } 9 | 10 | export class ConditionalVariable { 11 | done: boolean 12 | maxWaitTime?: number // In Milliseconds 13 | sleepInterval: number // In Milliseconds 14 | 15 | constructor(sleepInterval = 500, maxWaitTime?: number) { 16 | this.done = false 17 | this.maxWaitTime = maxWaitTime 18 | this.sleepInterval = sleepInterval 19 | } 20 | 21 | notify() { 22 | this.done = true 23 | } 24 | 25 | async wait(): Promise { 26 | let sum = 0 27 | while (this.done === false) { 28 | if (this.maxWaitTime && sum >= this.maxWaitTime) { 29 | return false 30 | } 31 | 32 | await sleep(this.sleepInterval) 33 | sum += this.sleepInterval 34 | } 35 | 36 | return true 37 | } 38 | 39 | isDone() { 40 | return this.done 41 | } 42 | 43 | clear() { 44 | this.done = false 45 | } 46 | } 47 | 48 | export class BlockInfo { 49 | block?: Block 50 | blockHash?: string 51 | 52 | constructor(block?: Block, blockHash?: string) { 53 | this.block = block 54 | this.blockHash = blockHash 55 | } 56 | } 57 | 58 | export const blockNumberToDate = async (blockNumber: number) => { 59 | if (blockNumber <= 0) throw new Error(Errors.VALUE_MUST_BE_GREATER_THAN_0) 60 | const api = getRawApi() 61 | const lastBlockDatas = await api.rpc.chain.getBlock() 62 | const lastBlockNumber = Number(lastBlockDatas.block.header.number.toString()) 63 | if (blockNumber > lastBlockNumber) throw new Error(Errors.BLOCK_NOT_FOUND_ON_CHAIN) 64 | const blockHash = await api.rpc.chain.getBlockHash(blockNumber) 65 | const signedBlock = await api.rpc.chain.getBlock(blockHash) 66 | const timestampNow = signedBlock.block.extrinsics[0].method.args 67 | const formatedTimestamp = timestampNow.toString().slice(0, -3) 68 | const date = new Date(Number(formatedTimestamp) * 1000) 69 | return date 70 | } 71 | -------------------------------------------------------------------------------- /src/tee/storage.ts: -------------------------------------------------------------------------------- 1 | import { query } from "../blockchain" 2 | import { chainQuery, Errors, txPallets } from "../constants" 3 | 4 | import { ClusterDataType, EnclaveDataType } from "./types" 5 | 6 | /** 7 | * @name getClusterData 8 | * @summary Provides the data related to a cluster. 9 | * @param clusterId The Cluster id. 10 | * @returns An array containing the cluster data: the list of enclaves 11 | */ 12 | export const getClusterData = async (clusterId: number): Promise => { 13 | const data = await query(txPallets.tee, chainQuery.clusterData, [clusterId]) 14 | if (data.isEmpty == true) { 15 | throw new Error(`${Errors.TEE_CLUSTER_NOT_FOUND}: ${clusterId}`) 16 | } 17 | try { 18 | const result = data.toJSON() as ClusterDataType 19 | return result 20 | } catch (error) { 21 | throw new Error(`${Errors.CLUSTER_CONVERSION_ERROR}`) 22 | } 23 | } 24 | 25 | /** 26 | * @name getEnclaveData 27 | * @summary Provides the data related to an enclave. 28 | * @param enclaveId The Enclave id. 29 | * @returns A JSON object with the enclave data. ex:{enclaveAddress, apiUri (...)} 30 | */ 31 | export const getEnclaveData = async (enclaveId: string): Promise => { 32 | const data = await query(txPallets.tee, chainQuery.enclaveData, [enclaveId]) 33 | if (data.isEmpty == true) { 34 | throw new Error(`${Errors.TEE_ENCLAVE_NOT_FOUND}: ${enclaveId}`) 35 | } 36 | try { 37 | const result = data.toJSON() as EnclaveDataType 38 | return result 39 | } catch (error) { 40 | throw new Error(`${Errors.ENCLAVE_CONVERSION_ERROR}`) 41 | } 42 | } 43 | 44 | /** 45 | * @name getNextClusterIdAvailable 46 | * @summary Provides the next available cluster id. 47 | * @returns A number corresponding to the next available cluster id. 48 | */ 49 | export const getNextClusterIdAvailable = async () => { 50 | try { 51 | const data = await query(txPallets.tee, chainQuery.nextClusterId) 52 | if (data.isEmpty == true) { 53 | throw new Error(`${Errors.NEXT_TEE_CLUSTER_UNDEFINED}`) 54 | } 55 | return data.toHuman() as number 56 | } catch (error) { 57 | throw new Error(`${Errors.NEXT_TEE_CLUSTER_UNDEFINED}`) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ternoa-js", 3 | "version": "1.6.5", 4 | "description": "Ternoa library of functions to interact with the blockchain and manipulate transactions", 5 | "engines": { 6 | "node": ">=14.13.1 || >=16.0.0" 7 | }, 8 | "main": "./src/index.ts", 9 | "scripts": { 10 | "start": "ts-node src/index.ts", 11 | "dev": "nodemon src/index.ts", 12 | "build": "rimraf build && tsc && node ./src/_misc/scripts/build.mjs", 13 | "docs": "typedoc ./src/index.ts", 14 | "lint": "eslint ./src --ext .js,.ts", 15 | "format": "prettier --write \"src/**/*.ts\"", 16 | "test": "npx jest --config jest.config.ts -i" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/capsule-corp-ternoa/ternoa-js.git" 21 | }, 22 | "keywords": [ 23 | "ternoa", 24 | "blockchain", 25 | "transaction", 26 | "sdk", 27 | "substrate" 28 | ], 29 | "author": "Ghali Leouarz", 30 | "contributors": [ 31 | { 32 | "name": "Igor Papandinas" 33 | }, 34 | { 35 | "name": "Victor Salomon" 36 | }, 37 | { 38 | "name": "Marko Petrlic" 39 | } 40 | ], 41 | "license": "Apache-2.0", 42 | "bugs": { 43 | "url": "https://github.com/capsule-corp-ternoa/ternoa-js/issues" 44 | }, 45 | "homepage": "https://github.com/capsule-corp-ternoa/ternoa-js#readme", 46 | "devDependencies": { 47 | "@jest/test-sequencer": "29.5.x", 48 | "@types/jest": "29.5.x", 49 | "@types/mime-types": "2.1.x", 50 | "@typescript-eslint/eslint-plugin": "^6.14.0", 51 | "@typescript-eslint/parser": "^6.14.0", 52 | "dotenv": "16.0.x", 53 | "eslint": "^8.55.0", 54 | "eslint-config-prettier": "^9.1.0", 55 | "eslint-plugin-jest": "^27.6.0", 56 | "jest": "29.5.x", 57 | "nodemon": "^3.0.2", 58 | "prettier": "^3.1.1", 59 | "ts-jest": "29.1.x", 60 | "ts-node": "10.9.x", 61 | "typedoc": "^0.25.4", 62 | "typescript": "^5.3.3" 63 | }, 64 | "dependencies": { 65 | "@polkadot/api": "^10.10.1", 66 | "axios": "^1.6.2", 67 | "bn.js": "5.2.x", 68 | "buffer": "6.0.x", 69 | "form-data-encoder": "1.7.x", 70 | "formdata-node": "4.4.x", 71 | "openpgp": "^5.11.0", 72 | "sssa-js": "^0.0.1", 73 | "stream": "0.0.x" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | title: Bug report 4 | labels: ['bug (-﹏-。)'] 5 | body: 6 | - type: textarea 7 | id: bug_report_description 8 | attributes: 9 | label: Describe the bug 10 | description: A clear and concise description of what the bug is 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: bug_report_reproduce 15 | attributes: 16 | label: To reproduce 17 | description: Steps to reproduce the behavior 18 | placeholder: | 19 | 1. Go to '...' 20 | 2. Click on '...' 21 | 3. Scroll down to '...' 22 | 4. See error 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: bug_report_expected_behavior 27 | attributes: 28 | label: Expected behavior 29 | description: A clear and concise description of what you expected to happen 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: bug_report_screenshots 34 | attributes: 35 | label: Screenshots 36 | description: | 37 | If applicable, add screenshots to help explain your problem 38 | Tip: You can attach images by clicking this area to highlight it and then dragging files in. 39 | - type: textarea 40 | id: bug_report_desktop 41 | attributes: 42 | label: Desktop (please complete the following information) 43 | placeholder: | 44 | - OS: [e.g. iOS] 45 | - Browser: [e.g. chrome, safari] 46 | - Version: [e.g. 22] 47 | - type: textarea 48 | id: bug_report_smartphone 49 | attributes: 50 | label: Smartphone (please complete the following information) 51 | placeholder: | 52 | - Device: [e.g. iPhone6] 53 | - OS: [e.g. iOS8.1] 54 | - Browser: [e.g. stock browser, safari] 55 | - Version: [e.g. 22] 56 | - type: textarea 57 | id: bug_report_additional_context 58 | attributes: 59 | label: Additional context 60 | description: Add any other context about the problem here 61 | - type: markdown 62 | attributes: 63 | value: | 64 | **Want to contribute?** 65 | We love contributions from the Ternoa community! Please comment on an issue if you're interested in helping out with a PR. 66 | -------------------------------------------------------------------------------- /src/helpers/crypto.ts: -------------------------------------------------------------------------------- 1 | import { IKeyringPair } from "@polkadot/types/types" 2 | import { SignerResult, Signer } from "@polkadot/api/types" 3 | import { u8aToHex } from "@polkadot/util" 4 | import { getRawApi } from "../blockchain" 5 | import { Buffer } from "buffer" 6 | 7 | /** 8 | * @name getSignatureFromKeyring 9 | * @summary Signs data using the keyring. 10 | * @param keyring Account that will sign the data. 11 | * @param data Data to be signed. 12 | * @returns Hex value of the signed data. 13 | */ 14 | export const getSignatureFromKeyring = (keyring: IKeyringPair, data: string) => { 15 | const finalData = new Uint8Array(Buffer.from(data)) 16 | return u8aToHex(keyring.sign(finalData)) 17 | } 18 | 19 | /** 20 | * @name getSignatureFromExtension 21 | * @summary Signs data using an injector extension. We recommand Polkadot extention. 22 | * @param signerAddress Account address that will sign the data. 23 | * @param injectorExtension The signer method retrived from your extension: object must have a signer key. 24 | * @param data Data to be signed. 25 | * @returns Hex value of the signed data. 26 | */ 27 | export const getSignatureFromExtension = async ( 28 | signerAddress: string, 29 | injectorExtension: Record, 30 | data: string, 31 | ) => { 32 | // To handle Polkadot Extension 33 | if (injectorExtension && "signer" in injectorExtension && injectorExtension.signer.signRaw) { 34 | const { signature } = (await injectorExtension.signer.signRaw({ 35 | address: signerAddress, 36 | data, 37 | type: "payload", 38 | })) as SignerResult 39 | return signature 40 | } 41 | // To handle signing from Api 42 | else if (injectorExtension && "signer" in injectorExtension) { 43 | const api = getRawApi() 44 | return await api.sign( 45 | signerAddress, 46 | { 47 | data: data, 48 | }, 49 | { signer: injectorExtension.signer }, 50 | ) 51 | } 52 | } 53 | 54 | /** 55 | * @name getLastBlock 56 | * @summary Retrieve the last block number. 57 | * @returns The last Block id (a number). 58 | */ 59 | export const getLastBlock = async () => { 60 | const api = getRawApi() 61 | const lastBlockHash = await api.rpc.chain.getFinalizedHead() 62 | const lastBlock = await api.rpc.chain.getBlock(lastBlockHash) 63 | const lastBlockId = Number(lastBlock.block.header.number.toString()) 64 | return lastBlockId 65 | } 66 | -------------------------------------------------------------------------------- /src/marketplace/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeApi } from "../blockchain" 2 | import { WaitUntil } from "../constants" 3 | import { createNft } from "../nft" 4 | import { createTestPairs } from "../_misc/testingPairs" 5 | import { MarketplaceKind } from "./enum" 6 | import { createMarketplace, listNft } from "./extrinsics" 7 | import { getMarketplaceMintFee, getNextMarketplaceId, getMarketplaceData, getNftForSale } from "./storage" 8 | 9 | const TEST_DATA = { 10 | marketplaceId: 0, 11 | nftId: 0, 12 | } 13 | 14 | beforeAll(async () => { 15 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 16 | await initializeApi(endpoint) 17 | 18 | // Create some data to test Marketplace queries. 19 | const { test: testAccount } = await createTestPairs() 20 | const nEvent = await createNft("Test NFT Data", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 21 | const mEvent = await createMarketplace(MarketplaceKind.Public, testAccount, WaitUntil.BlockInclusion) 22 | TEST_DATA.nftId = nEvent.nftId 23 | TEST_DATA.marketplaceId = mEvent.marketplaceId 24 | }) 25 | 26 | it("Marketplace Mint Fee storage should exist and it should not be null", async () => { 27 | const actual = await getMarketplaceMintFee() 28 | expect(actual != undefined).toBe(true) 29 | }) 30 | 31 | it("Next Marketplace ID storage should exist and it should not be null", async () => { 32 | const actual = await getNextMarketplaceId() 33 | expect(actual != undefined).toBe(true) 34 | }) 35 | 36 | describe("Testing Marketplace data", (): void => { 37 | it("Should return null if an invalid Marketplace ID is passed", async () => { 38 | const maybeMarketplace = await getMarketplaceData(1000000) 39 | expect(maybeMarketplace).toBeNull() 40 | }) 41 | it("Should return the NFT Data when the Marketplace ID exists", async () => { 42 | const maybeMarketplace = await getMarketplaceData(TEST_DATA.marketplaceId) 43 | expect(maybeMarketplace != null).toBe(true) 44 | }) 45 | }) 46 | 47 | describe("Testing NFT for sale data", (): void => { 48 | it("Should return null if an invalid NFT ID is passed", async () => { 49 | const maybeNFTListed = await getNftForSale(1000000) 50 | expect(maybeNFTListed).toBeNull() 51 | }) 52 | it("Should return the NFT Data when the Marketplace ID exists", async () => { 53 | const { test: testAccount } = await createTestPairs() 54 | await listNft(TEST_DATA.nftId, TEST_DATA.marketplaceId, 13, testAccount, WaitUntil.BlockInclusion) 55 | const maybeNFTListed = await getNftForSale(TEST_DATA.nftId) 56 | expect(maybeNFTListed != null).toBe(true) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/auction/constants.ts: -------------------------------------------------------------------------------- 1 | import { consts } from "../blockchain" 2 | import { chainConstants, txPallets } from "../constants" 3 | 4 | /** 5 | * @name getAuctionEndingPeriod 6 | * @summary Period (in blocks) before the end of the auction during which an auction can be extended if new bids are added. 7 | * @returns Number. 8 | */ 9 | export const getAuctionEndingPeriod = (): number => { 10 | const endingPeriod = consts(txPallets.auction, chainConstants.auctionEndingPeriod) 11 | return Number.parseInt(endingPeriod.toString()) 12 | } 13 | 14 | /** 15 | * @name getAuctionGracePeriod 16 | * @summary Period (in blocks) to extend an auction by if a new bid is received during the ending period. 17 | * @returns Number. 18 | */ 19 | export const getAuctionGracePeriod = (): number => { 20 | const gracePeriod = consts(txPallets.auction, chainConstants.auctionGracePeriod) 21 | return Number.parseInt(gracePeriod.toString()) 22 | } 23 | 24 | /** 25 | * @name getBidderListLengthLimit 26 | * @summary Total amount of accounts that can be in the bidder list for an auction. 27 | * @returns Number. 28 | */ 29 | export const getBidderListLengthLimit = (): number => { 30 | const limit = consts(txPallets.auction, chainConstants.bidderListLengthLimit) 31 | return Number.parseInt(limit.toString()) 32 | } 33 | 34 | /** 35 | * @name getMaxAuctionDelay 36 | * @summary Maximum amount of blocks between the current one and the start block of an auction. 37 | * @returns Number. 38 | */ 39 | export const getMaxAuctionDelay = (): number => { 40 | const maxDelay = consts(txPallets.auction, chainConstants.maxAuctionDelay) 41 | return Number.parseInt(maxDelay.toString()) 42 | } 43 | 44 | /** 45 | * @name getMinAuctionDuration 46 | * @summary Minimum amount of blocks permitted for an auction's length. 47 | * @returns Number. 48 | */ 49 | export const getMinAuctionDuration = (): number => { 50 | const minDuration = consts(txPallets.auction, chainConstants.minAuctionDuration) 51 | return Number.parseInt(minDuration.toString()) 52 | } 53 | 54 | /** 55 | * @name getMaxAuctionDuration 56 | * @summary Maximum amount of blocks permitted for an auction's length. 57 | * @returns Number. 58 | */ 59 | export const getMaxAuctionDuration = (): number => { 60 | const maxDuration = consts(txPallets.auction, chainConstants.maxAuctionDuration) 61 | return Number.parseInt(maxDuration.toString()) 62 | } 63 | 64 | /** 65 | * @name getParallelAuctionLimit 66 | * @summary Maximum amount of auctions that can be active at the same time. 67 | * @returns Number. 68 | */ 69 | export const getParallelAuctionLimit = (): number => { 70 | const limit = consts(txPallets.auction, chainConstants.parallelAuctionLimit) 71 | return Number.parseInt(limit.toString()) 72 | } 73 | -------------------------------------------------------------------------------- /src/balance/storage.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | 3 | import { chainQuery, txPallets } from "../constants" 4 | import { query, numberToBalance } from "../blockchain" 5 | import { Balances } from "./types" 6 | 7 | /** 8 | * @name getBalances 9 | * @summary Get the balances of an account including free & reserved balances as well as the total. 10 | * Currently Mainnet also returns miscFrozen & feeFrozen while alphanet returns frozen and flags. After next Mainnet runtime upgrade both miscFrozen & feeFrozen will be removed. 11 | * @param address Public address of the account to get balances. 12 | * @returns The balances of the account. 13 | */ 14 | export const getBalances = async (address: string): Promise => { 15 | const balances: Balances = ((await query(txPallets.system, chainQuery.account, [address])) as any).data 16 | return balances 17 | } 18 | 19 | /** 20 | * @name getTotalBalance 21 | * @summary Get the total balance of an account (free & reserve balances) 22 | * @param address Public address of the account to get total balance for. 23 | * @returns The total balance of an account (free & reserve balances) 24 | */ 25 | export const getTotalBalance = async (address: string): Promise => { 26 | const { free, reserved } = await getBalances(address) 27 | return free.add(reserved) 28 | } 29 | 30 | /** 31 | * @name getTransferrableBalance 32 | * @summary Get the transferrable balance of an account. 33 | * @param address Public address of the account to get transferrable balance for. 34 | * @returns The transferrable balance of an account. 35 | */ 36 | export const getTransferrableBalance = async (address: string): Promise => { 37 | const { free, miscFrozen, feeFrozen, frozen } = await getBalances(address) 38 | 39 | let totalFrozen 40 | 41 | if (miscFrozen !== undefined && feeFrozen !== undefined) { 42 | if (feeFrozen.gt(miscFrozen)) { 43 | totalFrozen = feeFrozen 44 | } else { 45 | totalFrozen = miscFrozen 46 | } 47 | return free.sub(totalFrozen) 48 | } else if (frozen) { 49 | return free.sub(frozen) 50 | } 51 | return free 52 | } 53 | 54 | /** 55 | * @name checkBalanceForTransfer 56 | * @summary Check if an account as enough funds to ensure a transfer. 57 | * @param address Public address of the account to check balance for transfer. 58 | * @param value Token amount to check before transfer. 59 | */ 60 | export const checkBalanceForTransfer = async (address: string, value: number | BN): Promise => { 61 | const amount = typeof value === "number" ? numberToBalance(value) : value 62 | const { free } = await getBalances(address) 63 | 64 | return free.gt(amount) 65 | } 66 | -------------------------------------------------------------------------------- /src/helpers/types.ts: -------------------------------------------------------------------------------- 1 | export type PGPKeysType = { 2 | privateKey: string 3 | publicKey: string 4 | } 5 | 6 | export interface IServiceIPFS { 7 | apiKey?: string 8 | apiUrl: URL 9 | } 10 | 11 | export type IpfsAddDataResponseType = { 12 | Bytes?: number 13 | Hash: string 14 | Name: string 15 | Size: string 16 | } 17 | 18 | export type NftMetadataType = { 19 | title: string 20 | description: string 21 | [key: string]: unknown 22 | properties?: { 23 | [key: string]: unknown 24 | media?: { 25 | [key: string]: unknown 26 | } 27 | } 28 | } 29 | 30 | export type MediaMetadataType = { 31 | name?: string 32 | description?: string 33 | [key: string]: unknown 34 | } 35 | 36 | export type CollectionMetadataType = Required 37 | 38 | export type MarketplaceMetadataType = Omit, "description"> 39 | 40 | export type RequesterType = "OWNER" | "DELEGATEE" | "RENTEE" 41 | 42 | export type CapsuleMedia = { 43 | encryptedFile: string 44 | type: string 45 | [key: string]: unknown 46 | } 47 | 48 | export type CapsuleEncryptedMedia = { 49 | hash: string 50 | type: string 51 | size: number 52 | } 53 | 54 | export type StorePayloadType = { 55 | owner_address: string 56 | signer_address: string 57 | data: string 58 | signature: string 59 | signersig: string 60 | } 61 | 62 | export type RetrievePayloadType = { 63 | requester_address: string 64 | requester_type: RequesterType 65 | data: string 66 | signature: string 67 | } 68 | 69 | export type TeeGenericDataResponseType = { 70 | status: string 71 | nft_id: number 72 | enclave_id: string 73 | description: string 74 | } 75 | 76 | export type TeeRetrieveDataResponseType = { 77 | status: string 78 | nft_id?: number 79 | keyshare_data: string 80 | enclave_id: string 81 | description: string 82 | } 83 | 84 | export type TeeSharesStoreType = { 85 | isError: boolean 86 | enclave_id?: string 87 | enclaveAddress: string 88 | operatorAddress: string 89 | enclaveSlot: number 90 | } & Omit & 91 | StorePayloadType 92 | 93 | export type RetryUploadErrorType = { 94 | isRetryError: boolean 95 | status: string 96 | message: string 97 | } 98 | 99 | export type TeeSharesRemoveType = { 100 | requester_address: string 101 | nft_id: number 102 | } 103 | 104 | export type ReconciliationPayloadType = { 105 | metric_account: string 106 | block_interval: string 107 | auth_token: string 108 | signature: string 109 | } 110 | 111 | export type NFTListType = { 112 | nftid: number[] 113 | } 114 | 115 | export type TeeReconciliationType = { 116 | enclaveAddress: string 117 | operatorAddress: string 118 | nftId: number[] 119 | error?: string 120 | } 121 | -------------------------------------------------------------------------------- /src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | import { File } from "formdata-node" 3 | import { Buffer } from "buffer" 4 | 5 | import { balanceToNumber } from "../blockchain" 6 | import { Errors } from "../constants" 7 | import { RetryUploadErrorType } from "./types" 8 | 9 | /** 10 | * @name convertFileToBuffer 11 | * @summary Converts a File to Buffer. 12 | * @param file File to convert. 13 | * @returns A Buffer. 14 | */ 15 | export const convertFileToBuffer = async (file: File): Promise => { 16 | const arrayBuffer = await file.arrayBuffer() 17 | const buffer = Buffer.from(arrayBuffer) 18 | 19 | return buffer 20 | } 21 | 22 | /** 23 | * @name formatPermill 24 | * @summary Checks that percent is in range 0 to 100 and format to permill. 25 | * @param percent Number in range from 0 to 100 with max 4 decimals. 26 | * @returns The formated percent in permill format. 27 | */ 28 | export const formatPermill = (percent: number): number => { 29 | if (percent > 100 || percent < 0) { 30 | throw new Error(Errors.MUST_BE_PERCENTAGE) 31 | } 32 | 33 | return parseFloat(percent.toFixed(4)) * 10000 34 | } 35 | 36 | export const roundBalance = (amount: string) => 37 | Number(balanceToNumber(new BN(amount), { forceUnit: "-", withUnit: false }).split(",").join("")) 38 | 39 | export const removeURLSlash = (url: string) => { 40 | if (url.length === 0) return url 41 | const lastChar = url.charAt(url.length - 1) 42 | if (lastChar === "/") { 43 | return url.slice(0, -1) 44 | } else { 45 | return url 46 | } 47 | } 48 | 49 | export const retryPost = async (fn: () => Promise, n: number): Promise => { 50 | let lastError: RetryUploadErrorType | undefined 51 | 52 | for (let i = 0; i < n; i++) { 53 | try { 54 | return await fn() 55 | } catch (err: any) { 56 | lastError = { 57 | isRetryError: true, 58 | status: "SDK_RETRY_POST_ERROR", 59 | message: err?.message ? err.message : JSON.stringify(err), 60 | } 61 | } 62 | } 63 | 64 | return lastError as RetryUploadErrorType 65 | } 66 | 67 | export const ensureHttps = (url: string) => { 68 | if (!url) throw new Error(Errors.URL_UNDEFINED) 69 | if (url.indexOf("https://") === 0) return url 70 | else if (url.indexOf("http://") === 0) return url.replace("http://", "https://") 71 | else return "https://" + url 72 | } 73 | 74 | export const timeoutTrigger = (fn: () => Promise, duration = 10000): Promise => { 75 | return new Promise((resolve, reject) => { 76 | const timer = setTimeout(() => { 77 | reject(new Error("Error: Function timed out")) 78 | }, duration) 79 | 80 | try { 81 | const data = fn() 82 | clearTimeout(timer) 83 | resolve(data) 84 | } catch (error) { 85 | clearTimeout(timer) 86 | reject(error) 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #001080; 9 | --dark-hl-3: #9CDCFE; 10 | --light-hl-4: #008000; 11 | --dark-hl-4: #6A9955; 12 | --light-hl-5: #0000FF; 13 | --dark-hl-5: #569CD6; 14 | --light-hl-6: #0070C1; 15 | --dark-hl-6: #4FC1FF; 16 | --light-hl-7: #AF00DB; 17 | --dark-hl-7: #C586C0; 18 | --light-hl-8: #000000; 19 | --dark-hl-8: #C8C8C8; 20 | --light-hl-9: #098658; 21 | --dark-hl-9: #B5CEA8; 22 | --light-code-background: #FFFFFF; 23 | --dark-code-background: #1E1E1E; 24 | } 25 | 26 | @media (prefers-color-scheme: light) { :root { 27 | --hl-0: var(--light-hl-0); 28 | --hl-1: var(--light-hl-1); 29 | --hl-2: var(--light-hl-2); 30 | --hl-3: var(--light-hl-3); 31 | --hl-4: var(--light-hl-4); 32 | --hl-5: var(--light-hl-5); 33 | --hl-6: var(--light-hl-6); 34 | --hl-7: var(--light-hl-7); 35 | --hl-8: var(--light-hl-8); 36 | --hl-9: var(--light-hl-9); 37 | --code-background: var(--light-code-background); 38 | } } 39 | 40 | @media (prefers-color-scheme: dark) { :root { 41 | --hl-0: var(--dark-hl-0); 42 | --hl-1: var(--dark-hl-1); 43 | --hl-2: var(--dark-hl-2); 44 | --hl-3: var(--dark-hl-3); 45 | --hl-4: var(--dark-hl-4); 46 | --hl-5: var(--dark-hl-5); 47 | --hl-6: var(--dark-hl-6); 48 | --hl-7: var(--dark-hl-7); 49 | --hl-8: var(--dark-hl-8); 50 | --hl-9: var(--dark-hl-9); 51 | --code-background: var(--dark-code-background); 52 | } } 53 | 54 | :root[data-theme='light'] { 55 | --hl-0: var(--light-hl-0); 56 | --hl-1: var(--light-hl-1); 57 | --hl-2: var(--light-hl-2); 58 | --hl-3: var(--light-hl-3); 59 | --hl-4: var(--light-hl-4); 60 | --hl-5: var(--light-hl-5); 61 | --hl-6: var(--light-hl-6); 62 | --hl-7: var(--light-hl-7); 63 | --hl-8: var(--light-hl-8); 64 | --hl-9: var(--light-hl-9); 65 | --code-background: var(--light-code-background); 66 | } 67 | 68 | :root[data-theme='dark'] { 69 | --hl-0: var(--dark-hl-0); 70 | --hl-1: var(--dark-hl-1); 71 | --hl-2: var(--dark-hl-2); 72 | --hl-3: var(--dark-hl-3); 73 | --hl-4: var(--dark-hl-4); 74 | --hl-5: var(--dark-hl-5); 75 | --hl-6: var(--dark-hl-6); 76 | --hl-7: var(--dark-hl-7); 77 | --hl-8: var(--dark-hl-8); 78 | --hl-9: var(--dark-hl-9); 79 | --code-background: var(--dark-code-background); 80 | } 81 | 82 | .hl-0 { color: var(--hl-0); } 83 | .hl-1 { color: var(--hl-1); } 84 | .hl-2 { color: var(--hl-2); } 85 | .hl-3 { color: var(--hl-3); } 86 | .hl-4 { color: var(--hl-4); } 87 | .hl-5 { color: var(--hl-5); } 88 | .hl-6 { color: var(--hl-6); } 89 | .hl-7 { color: var(--hl-7); } 90 | .hl-8 { color: var(--hl-8); } 91 | .hl-9 { color: var(--hl-9); } 92 | pre, code { background: var(--code-background); } 93 | -------------------------------------------------------------------------------- /src/balance/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestPairs } from "../_misc/testingPairs" 2 | import { generateSeed, getKeyringFromSeed } from "../account" 3 | import { initializeApi } from "../blockchain" 4 | import { checkBalanceForTransfer, getBalances, getTotalBalance, getTransferrableBalance } from "./storage" 5 | 6 | beforeAll(async () => { 7 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 8 | await initializeApi(endpoint) 9 | }) 10 | 11 | describe("Testing getBalances", (): void => { 12 | it("Should get an empty account free balance for a new one", async (): Promise => { 13 | const seed = generateSeed() 14 | const keyring = await getKeyringFromSeed(seed) 15 | const balance = await getBalances(keyring.address) 16 | expect(balance.free.isZero()).toBe(true) 17 | }) 18 | 19 | it("Should get a positive account free balance on the testing account", async (): Promise => { 20 | const { test: testAccount } = await createTestPairs() 21 | const balance = await getBalances(testAccount.address) 22 | expect(balance.free.isZero()).toBe(false) 23 | }) 24 | }) 25 | 26 | describe("Testing getTotalBalance", (): void => { 27 | it("Should get an empty account balance for a new one", async (): Promise => { 28 | const seed = generateSeed() 29 | const keyring = await getKeyringFromSeed(seed) 30 | const balance = await getTotalBalance(keyring.address) 31 | expect(balance.isZero()).toBe(true) 32 | }) 33 | 34 | it("Should get a positive account balance on the testing account", async (): Promise => { 35 | const { test: testAccount } = await createTestPairs() 36 | const balance = await getTotalBalance(testAccount.address) 37 | expect(balance.isZero()).toBe(false) 38 | }) 39 | }) 40 | 41 | describe("Testing getTransferrableBalance", (): void => { 42 | it("Should get an empty account balance for a new one", async (): Promise => { 43 | const seed = generateSeed() 44 | const keyring = await getKeyringFromSeed(seed) 45 | const balance = await getTransferrableBalance(keyring.address) 46 | expect(balance.isZero()).toBe(true) 47 | }) 48 | 49 | it("Should get a positive account balance on the testing account", async (): Promise => { 50 | const { test: testAccount } = await createTestPairs() 51 | const balance = await getTransferrableBalance(testAccount.address) 52 | expect(balance.isZero()).toBe(false) 53 | }) 54 | }) 55 | 56 | describe("Testing checkBalanceForTransfer", (): void => { 57 | it("Insufficient funds to transfer", async (): Promise => { 58 | const seed = generateSeed() 59 | const keyring = await getKeyringFromSeed(seed) 60 | const hasEnoughFunds = await checkBalanceForTransfer(keyring.address, 1) 61 | expect(hasEnoughFunds).toBe(false) 62 | }) 63 | 64 | it("Sufficient funds to transfer", async (): Promise => { 65 | const { test: testAccount } = await createTestPairs() 66 | const hasEnoughFunds = await checkBalanceForTransfer(testAccount.address, 1) 67 | expect(hasEnoughFunds).toBe(true) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/auction/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestPairs } from "../_misc/testingPairs" 2 | import { initializeApi, numberToBalance, query } from "../blockchain" 3 | import { chainQuery, txPallets, WaitUntil } from "../constants" 4 | import { createNft } from "../nft" 5 | import { createMarketplace } from "../marketplace" 6 | import { MarketplaceKind } from "../marketplace/enum" 7 | 8 | import { getMinAuctionDuration } from "./constants" 9 | import { createAuction } from "./extrinsics" 10 | import { getAuctionData, getAuctionDeadline } from "./storage" 11 | 12 | const TEST_DATA = { 13 | marketplaceId: 3, 14 | nftId: 0, 15 | startBlock: 0, 16 | endBlock: 0, 17 | } 18 | 19 | beforeAll(async () => { 20 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 21 | await initializeApi(endpoint) 22 | 23 | // Create some Test NFT, Marketplace and Auction 24 | const { test: testAccount } = await createTestPairs() 25 | const nEvent = await createNft("Test Auctioned NFT", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 26 | const mEvent = await createMarketplace(MarketplaceKind.Public, testAccount, WaitUntil.BlockInclusion) 27 | const currentBlock = await query(txPallets.system, chainQuery.number) 28 | const auctionMinDuration = getMinAuctionDuration() 29 | const auctionStartBlock = Number.parseInt(currentBlock.toString()) + 10 30 | const auctionEndBlock = auctionStartBlock + auctionMinDuration 31 | await createAuction( 32 | nEvent.nftId, 33 | mEvent.marketplaceId, 34 | auctionStartBlock, 35 | auctionEndBlock, 36 | 1, 37 | undefined, 38 | testAccount, 39 | WaitUntil.BlockInclusion, 40 | ) 41 | TEST_DATA.nftId = nEvent.nftId 42 | TEST_DATA.marketplaceId = mEvent.marketplaceId 43 | TEST_DATA.startBlock = auctionStartBlock 44 | TEST_DATA.endBlock = auctionEndBlock 45 | }) 46 | 47 | describe("Testing getting Auction data", (): void => { 48 | it("Should return null if an Invalid NFT ID is passed", async () => { 49 | const maybeAuction = await getAuctionData(1000000) 50 | expect(maybeAuction).toBeNull() 51 | }) 52 | it("Should return the Auction Data when the NFT ID exists", async () => { 53 | const { test: testAccount } = await createTestPairs() 54 | const auction = await getAuctionData(TEST_DATA.nftId) 55 | const startPrice = numberToBalance(1).toString() 56 | 57 | expect( 58 | auction?.creator === testAccount.address && 59 | auction?.marketplaceId === TEST_DATA.marketplaceId && 60 | auction?.startPrice === startPrice && 61 | auction?.startPriceRounded === 1 && 62 | auction?.buyItPrice === null && 63 | auction?.buyItPriceRounded === null && 64 | auction?.isExtended === false && 65 | auction?.bidders.length === 0 && 66 | auction?.startBlock === TEST_DATA.startBlock && 67 | auction?.endBlock === TEST_DATA.endBlock, 68 | ).toBe(true) 69 | }) 70 | }) 71 | 72 | it("Should return the auction ending block", async () => { 73 | const endBlock = await getAuctionDeadline(TEST_DATA.nftId) 74 | expect(endBlock).toBe(TEST_DATA.endBlock) 75 | }) 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ternoa-js 2 | 3 | Do you want to contribute to the Ternoa SDK ? First of all, thank you for your interest and welcome. We want to build the best and friendly tool to build on top of Ternoa Chain and we appreciate your willingness to help us. 4 | 5 | ## Communication 6 | 7 | The development process aims to be as transparent as possible. All our ideas, design choices and tasks tracking are accessible publicly in our Github sections: 8 | 9 | - [Github Issues](https://github.com/capsule-corp-ternoa/ternoa-js/issues): The place to filing and track pending issues. 10 | - [Github Discussions](https://github.com/capsule-corp-ternoa/ternoa-js/discussions): The place to discover and build ternoa-js together. 11 | - [Github Projects](https://github.com/orgs/capsule-corp-ternoa/projects/8/views/5): The place to track the current tasks handled for the next incoming release. 12 | 13 | ## Filing Issues 14 | 15 | Did you discover a bug? Filing issues is an easy way anyone can contribute and helps us improve ternoa-js. We use [Github Issues](https://github.com/capsule-corp-ternoa/ternoa-js/issues) to track all known bugs and feature requests. 16 | 17 | Before logging an issue be sure to check current issues, also verify that your package version is [the latest one](https://www.npmjs.com/package/ternoa-js). Make sure to specify whether you are describing a bug or a new enhancement using the **Bug** or **Enhancement** label. 18 | 19 | If you have a new feature request, feel free to describe it in our [Github Discussions](https://github.com/capsule-corp-ternoa/ternoa-js/discussions) space opening a new discussion of type `Ideas`. 20 | 21 | See the GitHub help guide for more information on [filing an issue](https://help.github.com/en/articles/creating-an-issue). 22 | 23 | ## Rules 24 | 25 | There are a few basic ground-rules for contributors (including the maintainer(s) of the project): 26 | 27 | 1. **No `--force` pushes** or modifying the Git history in any way. If you need to rebase, ensure you do it in your own repo. 28 | 2. **All modifications** must be made in a **pull-request** to solicit feedback from other contributors. 29 | 3. A pull-request _must not be merged until CI_ has finished successfully. 30 | 31 | ## Contribution Model 32 | 33 | The `main` branch refers to the last stable release, the incoming work is handled on the `dev` branch. Each pull request should be initiated against the `dev` branch and must satisfy the CI rules which include unit tests. 34 | 35 | ## Code Guidelines 36 | 37 | This project uses recommended ESLint and Typescript rules to ensure coding good practices. 38 | 39 | We've setup linters and formatters to help catch errors and improve the development experience: 40 | 41 | - [Prettier](https://prettier.io/) – ensures that code is formatted in a readable way. 42 | - [ESLint](https://eslint.org/) — checks code for antipatterns as well as formatting. 43 | 44 | > If you use Visual Studio Code editor we suggest you to install [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) extensions. 45 | -------------------------------------------------------------------------------- /src/helpers/encryption.ts: -------------------------------------------------------------------------------- 1 | import * as openpgp from "openpgp" 2 | import { File } from "formdata-node" 3 | 4 | import { PGPKeysType } from "./types" 5 | import { convertFileToBuffer } from "./utils" 6 | 7 | /** 8 | * @name generatePGPKeys 9 | * @summary Generates a new PGP key pair. 10 | * @returns An object with both private and public PGP keys. 11 | */ 12 | export const generatePGPKeys = async (): Promise => { 13 | const { privateKey, publicKey } = await openpgp.generateKey({ 14 | type: "ecc", 15 | curve: "curve25519", 16 | userIDs: [{ name: "Jon Smith", email: "jon@example.com" }], 17 | }) 18 | 19 | return { privateKey, publicKey } 20 | } 21 | 22 | /** 23 | * @name encryptContent 24 | * @summary Encrypts a content (string). 25 | * @param content Content to encrypt. 26 | * @param publicPGPKey Public Key to encrypt the content. 27 | * @see Learn more about encryption {@link https://docs.openpgpjs.org/global.html#encrypt here}. 28 | * @returns A string containing the encrypted content. 29 | */ 30 | export const encryptContent = async (content: string, publicPGPKey: string) => { 31 | const message = await openpgp.createMessage({ 32 | text: content, 33 | }) 34 | const publicKey = await openpgp.readKey({ 35 | armoredKey: publicPGPKey, 36 | }) 37 | const encryptedContent = await openpgp.encrypt({ 38 | message, 39 | encryptionKeys: [publicKey], 40 | }) 41 | 42 | return encryptedContent as string 43 | } 44 | 45 | /** 46 | * @name encryptFile 47 | * @summary Encrypts file with the public key. 48 | * @param file File to encrypt. 49 | * @param publicPGPKey Public Key to encrypt the file. 50 | * @see Learn more about encryption {@link https://docs.openpgpjs.org/global.html#encrypt here}. 51 | * @returns A string containing the encrypted file. 52 | */ 53 | export const encryptFile = async (file: File, publicPGPKey: string) => { 54 | const buffer = await convertFileToBuffer(file) 55 | const content = buffer.toString("base64") 56 | const encryptedFile = await encryptContent(content, publicPGPKey) 57 | 58 | return encryptedFile 59 | } 60 | 61 | /** 62 | * @name decryptFile 63 | * @summary Decrypts message with the private key. 64 | * @param encryptedMessage Message to decrypt. 65 | * @param privatePGPKey Private Key to decrypt the message. 66 | * @see Learn more about encryption {@link https://docs.openpgpjs.org/global.html#decrypt here}. 67 | * @returns A base64 string containing the decrypted message. 68 | */ 69 | export const decryptFile = async (encryptedMessage: string, privatePGPKey: string) => { 70 | const privateKey = await openpgp.readPrivateKey({ armoredKey: privatePGPKey }) 71 | const message = await openpgp.readMessage({ 72 | armoredMessage: encryptedMessage, 73 | }) 74 | const { data: decryptedMessage } = await openpgp.decrypt({ 75 | message, 76 | decryptionKeys: privateKey, 77 | }) 78 | 79 | return decryptedMessage as string 80 | } 81 | -------------------------------------------------------------------------------- /src/marketplace/storage.ts: -------------------------------------------------------------------------------- 1 | import { bnToBn, hexToString } from "@polkadot/util" 2 | import BN from "bn.js" 3 | 4 | import { IListedNft, MarketplaceDataType } from "./types" 5 | 6 | import { query, balanceToNumber } from "../blockchain" 7 | import { chainQuery, Errors, txPallets } from "../constants" 8 | 9 | /** 10 | * @name getMarketplaceMintFee 11 | * @summary Fee to mint a Marketplace. (extra fee on top of the tx fees). 12 | * @returns Marketplace mint fee. 13 | */ 14 | export const getMarketplaceMintFee = async (): Promise => { 15 | const fee = await query(txPallets.marketplace, chainQuery.marketplaceMintFee) 16 | return fee as any as BN 17 | } 18 | 19 | /** 20 | * @name getNextMarketplaceId 21 | * @summary Get the next Marketplace Id available. 22 | * @returns Number. 23 | */ 24 | export const getNextMarketplaceId = async (): Promise => { 25 | const id = await query(txPallets.marketplace, chainQuery.nextMarketplaceId) 26 | return (id as any as BN).toNumber() 27 | } 28 | 29 | /** 30 | * @name getMarketplaceData 31 | * @summary Provides the data related to a marketplace. 32 | * @param marketplaceId The Markeplace id. 33 | * @returns A JSON object with the marketplace data. ex:{owner, kind, accountList, (...)} 34 | */ 35 | export const getMarketplaceData = async (marketplaceId: number): Promise => { 36 | const data = await query(txPallets.marketplace, chainQuery.marketplaces, [marketplaceId]) 37 | if (data.isEmpty == true) { 38 | return null 39 | } 40 | 41 | try { 42 | const result = data.toJSON() as MarketplaceDataType 43 | if (result.commissionFee) { 44 | result.commissionFee.flat 45 | ? (result.commissionFee.flat = balanceToNumber(bnToBn(result.commissionFee.flat))) 46 | : (result.commissionFee.percentage = result.commissionFee.percentage / 10000) 47 | } 48 | if (result.listingFee) { 49 | result.listingFee.flat 50 | ? (result.listingFee.flat = balanceToNumber(bnToBn(result.listingFee.flat))) 51 | : (result.listingFee.percentage = result.listingFee.percentage / 10000) 52 | } 53 | // The offchainData is an hexadecimal string, we convert it to a human readable string. 54 | if (result.offchainData) result.offchainData = hexToString(result.offchainData) 55 | return result 56 | } catch (error) { 57 | throw new Error(`${Errors.MARKETPLACE_CONVERSION_ERROR}`) 58 | } 59 | } 60 | 61 | /** 62 | * @name getNftForSale 63 | * @summary Provides the data related to an NFT listed for sale. 64 | * @param nftId The NFT id. 65 | * @returns A JSON object with the NFT listing data. 66 | */ 67 | export const getNftForSale = async (nftId: number): Promise => { 68 | const data = await query(txPallets.marketplace, chainQuery.listedNfts, [nftId]) 69 | if (data.isEmpty == true) { 70 | return null 71 | } 72 | 73 | try { 74 | const result = data.toJSON() as any as IListedNft 75 | return result 76 | } catch (error) { 77 | throw new Error(`${Errors.LISTED_NFT_CONVERSION_ERROR}`) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/auction/storage.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | import { bnToBn } from "@polkadot/util" 3 | import { roundBalance } from "../helpers/utils" 4 | import { query } from "../blockchain" 5 | import { chainQuery, Errors, txPallets } from "../constants" 6 | import { AuctionChainRawDataType, AuctionDataType, Bidder, ClaimableBidBalanceDataType } from "./types" 7 | 8 | /** 9 | * @name getAuctionData 10 | * @summary Provides the data related to an auction. 11 | * @param nftId The ID of the Auctioned NFT. 12 | * @returns A JSON object with the auction data. 13 | */ 14 | export const getAuctionData = async (nftId: number): Promise => { 15 | const data = await query(txPallets.auction, chainQuery.auctions, [nftId]) 16 | if (data.isEmpty) { 17 | return null 18 | } 19 | try { 20 | const { creator, startBlock, endBlock, startPrice, buyItPrice, bidders, marketplaceId, isExtended } = 21 | data.toJSON() as any as AuctionChainRawDataType 22 | 23 | const startPriceAmount = bnToBn(startPrice).toString() 24 | const buyItPriceAmount = buyItPrice && bnToBn(buyItPrice).toString() 25 | const startPriceRounded = roundBalance(startPriceAmount) 26 | const buyItPriceRounded = buyItPriceAmount !== null ? roundBalance(buyItPriceAmount) : buyItPriceAmount 27 | const formattedBidders: Bidder[] = bidders.list.map((bidder) => { 28 | const [address, bid] = bidder 29 | const amount = bnToBn(bid).toString() 30 | const amountRounded = roundBalance(amount) 31 | return { 32 | bidder: address, 33 | amount, 34 | amountRounded, 35 | } 36 | }) 37 | 38 | const auction: AuctionDataType = { 39 | creator, 40 | startBlock, 41 | endBlock, 42 | startPrice: startPriceAmount, 43 | startPriceRounded, 44 | buyItPrice: buyItPriceAmount, 45 | buyItPriceRounded, 46 | bidders: formattedBidders, 47 | marketplaceId, 48 | isExtended, 49 | } 50 | 51 | return auction 52 | } catch (error) { 53 | throw new Error(`${Errors.AUCTION_NFT_CONVERSION_ERROR}`) 54 | } 55 | } 56 | 57 | /** 58 | * @name getAuctionDeadline 59 | * @summary Provides the auction ending block. 60 | * @param nftId The ID of the Auctioned NFT. 61 | * @returns Number. 62 | */ 63 | export const getAuctionDeadline = async (nftId: number): Promise => { 64 | const data = await query(txPallets.auction, chainQuery.auctions, [nftId]) 65 | if (data.isEmpty) { 66 | return null 67 | } 68 | 69 | const { endBlock } = data.toJSON() as any as AuctionChainRawDataType 70 | return endBlock 71 | } 72 | 73 | /** 74 | * @name getClaimableBidBalance 75 | * @summary Bids balance claimable after an auction ends. 76 | * @param address The bidder address. 77 | * @returns Number. 78 | */ 79 | export const getClaimableBidBalance = async (address: string): Promise => { 80 | const data = await query(txPallets.auction, chainQuery.claims, [address]) 81 | const parsedData = data.toJSON() as any as BN 82 | const claimable = bnToBn(parsedData).toString() 83 | const claimableRounded = roundBalance(claimable) 84 | 85 | return { 86 | claimable, 87 | claimableRounded, 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/rent/types.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | import { 3 | AcceptanceAction, 4 | CancellationFeeAction, 5 | DurationAction, 6 | RentFeeAction, 7 | SubscriptionActionDetails, 8 | } from "./enum" 9 | 10 | export type DurationSubscriptionDetailsType = { 11 | [SubscriptionActionDetails.PeriodLength]: number 12 | [SubscriptionActionDetails.MaxDuration]: number | null 13 | [SubscriptionActionDetails.IsChangeable]: boolean 14 | [SubscriptionActionDetails.NewTerms]?: boolean 15 | } 16 | export type DurationFixedType = { [DurationAction.Fixed]: number } 17 | export type DurationSubscriptionType = { [DurationAction.Subscription]: DurationSubscriptionDetailsType } 18 | export type DurationType = DurationFixedType | DurationSubscriptionType 19 | 20 | export type AutoAcceptanceType = { [AcceptanceAction.AutoAcceptance]: string[] | null } 21 | export type ManualAcceptanceType = { [AcceptanceAction.ManualAcceptance]: string[] | null } 22 | export type AcceptanceType = AutoAcceptanceType | ManualAcceptanceType 23 | 24 | export type RentFeeTokensType = { [RentFeeAction.Tokens]: number | BN } 25 | export type RentFeeNFTType = { [RentFeeAction.NFT]: number } 26 | export type RentFeeType = RentFeeTokensType | RentFeeNFTType 27 | 28 | export type CancellationFeeFixedTokensType = { [CancellationFeeAction.FixedTokens]: number | BN } 29 | export type CancellationFeeFlexibleTokensType = { [CancellationFeeAction.FlexibleTokens]: number | BN } 30 | export type CancellationFeeNFTType = { [CancellationFeeAction.NFT]: number } 31 | export type CancellationFeeType = 32 | | CancellationFeeAction.None 33 | | CancellationFeeFixedTokensType 34 | | CancellationFeeFlexibleTokensType 35 | | CancellationFeeNFTType 36 | 37 | export type RentalContractDataType = { 38 | creationBlock: number 39 | creationBlockDate: Date 40 | startBlock: number | null 41 | startBlockDate: Date | null 42 | renter: string 43 | rentee: string | null 44 | duration: DurationType 45 | acceptanceType: AcceptanceAction 46 | acceptanceList: string[] 47 | rentFeeType: RentFeeAction 48 | rentFee: string | number 49 | rentFeeRounded: number 50 | renterCanRevoke: boolean 51 | renterCancellationFeeType: CancellationFeeAction 52 | renterCancellationFee: string | number | null 53 | renterCancellationFeeRounded: number | null 54 | renteeCancellationFeeType: CancellationFeeAction 55 | renteeCancellationFee: string | number | null 56 | renteeCancellationFeeRounded: number | null 57 | } 58 | 59 | export type RentalContractChainRawDataType = { 60 | creationBlock: number 61 | startBlock: number | null 62 | renter: string 63 | rentee: string | null 64 | duration: any //DurationType 65 | acceptanceType: any //AcceptanceType 66 | renterCanRevoke: boolean 67 | rentFee: any //RentFeeType 68 | renterCancellationFee: any //CancellationFeeType 69 | renteeCancellationFee: any //CancellationFeeType 70 | } 71 | 72 | export type ActiveFixedContractType = { 73 | nftId: number 74 | endingBlockId: number 75 | } 76 | 77 | export type ActiveSubscribedContractType = { 78 | nftId: number 79 | renewalOrEndBlockId: number 80 | } 81 | 82 | export type AvailableRentalContractType = { 83 | nftId: number 84 | expirationBlockId: number 85 | } 86 | 87 | export type RentingQueuesType = { 88 | fixedQueue: ActiveFixedContractType[] 89 | subscriptionQueue: ActiveSubscribedContractType[] 90 | availableQueue: AvailableRentalContractType[] 91 | } 92 | 93 | export type RentingQueuesRawType = { 94 | fixedQueue: number[][] 95 | subscriptionQueue: number[][] 96 | availableQueue: number[][] 97 | } 98 | -------------------------------------------------------------------------------- /src/protocols/storage.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | import { query } from "../blockchain" 3 | import { chainQuery, Errors, txPallets } from "../constants" 4 | import { ProtocolAtBlockQueue, Transmissions } from "./types" 5 | 6 | /** 7 | * @name getTransmissionAtBlockFee 8 | * @summary Fee to set an AtBlock protocol. (extra fee on top of the tx fees). 9 | * @returns Transmission at block protocol fee. 10 | */ 11 | export const getTransmissionAtBlockFee = async (): Promise => { 12 | const fee = await query(txPallets.transmissionProtocols, chainQuery.atBlockFee) 13 | return fee as any as BN 14 | } 15 | 16 | /** 17 | * @name getTransmissionAtBlockWithResetFee 18 | * @summary Fee to set an AtBlockWithReset protocol. (extra fee on top of the tx fees). 19 | * @returns Transmission at block with reset protocol fee. 20 | */ 21 | export const getTransmissionAtBlockWithResetFee = async (): Promise => { 22 | const fee = await query(txPallets.transmissionProtocols, chainQuery.atBlockWithResetFee) 23 | return fee as any as BN 24 | } 25 | 26 | /** 27 | * @name getTransmissionOnConsentFee 28 | * @summary Fee to set an OnConsentFee protocol. (extra fee on top of the tx fees). 29 | * @returns Transmission on consent protocol fee. 30 | */ 31 | export const getTransmissionOnConsentFee = async (): Promise => { 32 | const fee = await query(txPallets.transmissionProtocols, chainQuery.onConsentFee) 33 | return fee as any as BN 34 | } 35 | 36 | /** 37 | * @name getTransmissionOnConsentAtBlockFee 38 | * @summary Fee to set an OnConsentAtBlockFee protocol. (extra fee on top of the tx fees). 39 | * @returns Transmission on consent at block protocol fee. 40 | */ 41 | export const getTransmissionOnConsentAtBlockFee = async (): Promise => { 42 | const fee = await query(txPallets.transmissionProtocols, chainQuery.onConsentAtBlockFee) 43 | return fee as any as BN 44 | } 45 | 46 | /** 47 | * @name getTransmissionAtBlockQueue 48 | * @summary Provides the deadlines related to at block transmission protocols in queues. 49 | * @returns An array of objects containing data related to tranmission queues. 50 | */ 51 | export const getTransmissionAtBlockQueue = async (): Promise => { 52 | try { 53 | const data = (await query(txPallets.transmissionProtocols, chainQuery.atBlockQueue)).toJSON() as [[number, number]] 54 | return data.map((queue) => ({ 55 | nftId: queue[0], 56 | blockNumber: queue[1], 57 | })) 58 | } catch (error) { 59 | throw new Error(`${Errors.TRANSMISSION_PROTOCOL_CONVERSION_ERROR}`) 60 | } 61 | } 62 | 63 | /** 64 | * @name getTransmissions 65 | * @summary Provides the data of a set transmission protocol. 66 | * @param nftId The ID of the NFT to be transmitted. 67 | * @returns An object containing data related to a tranmission protocol. 68 | */ 69 | export const getTransmissions = async (nftId: number): Promise => { 70 | const data = await query(txPallets.transmissionProtocols, chainQuery.transmissions, [nftId]) 71 | return data.toJSON() as Transmissions 72 | } 73 | 74 | /** 75 | * @name getTransmissionOnConsentData 76 | * @summary Provides the list of address that gave their consent to a transmission protocol. 77 | * @param nftId The ID of the NFT to check address that gave their consent. 78 | * @returns An array of the account address that gave their consent. 79 | */ 80 | export const getTransmissionOnConsentData = async (nftId: number): Promise => { 81 | const data = (await query(txPallets.transmissionProtocols, chainQuery.onConsentData, [nftId])).toJSON() 82 | return data as string[] 83 | } 84 | -------------------------------------------------------------------------------- /src/tee/extrinsics.ts: -------------------------------------------------------------------------------- 1 | import { IKeyringPair } from "@polkadot/types/types" 2 | import { ReportParamsType } from "./types" 3 | import { TransactionHashType, createTxHex, submitTxBlocking } from "../blockchain" 4 | import { txActions, txPallets, WaitUntil } from "../constants" 5 | import { MetricsServerReportSubmittedEvent, RewardsClaimedEvent } from "../events" 6 | 7 | /** 8 | * @name submitMetricsServerReportTx 9 | * @summary Creates an unsigned unsubmitted Submit Metrics Server Report Transaction Hash for an Era. 10 | * @param operatorAddress The operator address to which submitted scores belongs. 11 | * @param metricsServerReport The report containing the 5 scores computed for the mentioned era and the submitter's registered address. 12 | * @returns Unsigned unsubmitted Submit Metrics Server Report Transaction Hash. The Hash is only valid for 5 minutes. 13 | */ 14 | export const submitMetricsServerReportTx = async ( 15 | operatorAddress: string, 16 | metricsServerReport: ReportParamsType, 17 | ): Promise => { 18 | return await createTxHex(txPallets.tee, txActions.submitMetricsServerReport, [operatorAddress, metricsServerReport]) 19 | } 20 | 21 | /** 22 | * @name submitMetricsServerReport 23 | * @summary Submit the metrics server report for a specific era. 24 | * @param operatorAddress The operator address to which submitted scores belongs. 25 | * @param metricsServerReport The report containing the 5 scores computed for the mentioned era and the submitter's registered address. 26 | * @param keyring Account that will sign the transaction. 27 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 28 | * @returns MetricsServerReportSubmittedEvent Blockchain event. 29 | */ 30 | export const submitMetricsServerReport = async ( 31 | operatorAddress: string, 32 | metricsServerReport: ReportParamsType, 33 | keyring: IKeyringPair, 34 | waitUntil: WaitUntil, 35 | ): Promise => { 36 | const tx = await submitMetricsServerReportTx(operatorAddress, metricsServerReport) 37 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 38 | return events.findEventOrThrow(MetricsServerReportSubmittedEvent) 39 | } 40 | 41 | /** 42 | * @name claimTeeRewardsTx 43 | * @summary Creates an unsigned unsubmitted Claim Tee Rewards Transaction Hash for an Era. 44 | * @param era The era to claim the rewards. 45 | * @returns Unsigned unsubmitted Claim Tee Rewards Transaction Hash. The Hash is only valid for 5 minutes. 46 | */ 47 | export const claimTeeRewardsTx = async (era: number): Promise => { 48 | return await createTxHex(txPallets.tee, txActions.claimRewards, [era]) 49 | } 50 | 51 | /** 52 | * @name claimTeeRewards 53 | * @summary Claim the operator reward for a specific era. 54 | * @param era The era to claim the rewards. 55 | * @param keyring Account that will sign the transaction. 56 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 57 | * @returns RewardsClaimedEvent Blockchain event. 58 | */ 59 | export const claimTeeRewards = async ( 60 | era: number, 61 | keyring: IKeyringPair, 62 | waitUntil: WaitUntil, 63 | ): Promise => { 64 | const tx = await claimTeeRewardsTx(era) 65 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 66 | return events.findEventOrThrow(RewardsClaimedEvent) 67 | } 68 | -------------------------------------------------------------------------------- /src/protocols/extrinsics.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addConsentToOnConsentProtocol, 3 | removeTransmissionProtocol, 4 | resetTransmissionProtocolTimer, 5 | setTransmissionProtocol, 6 | } from "./extrinsics" 7 | import { initializeApi } from "../blockchain" 8 | import { createTestPairs } from "../_misc/testingPairs" 9 | import { WaitUntil } from "../constants" 10 | import { createNft } from "../nft" 11 | import { ProtocolAction, TransmissionCancellationAction } from "./enums" 12 | import { formatAtBlockWithResetProtocol, formatOnConsentProtocol, formatProtocolCancellation } from "./utils" 13 | import { getLastBlock } from "../helpers/crypto" 14 | const TEST_DATA = { 15 | nftId: 0, 16 | transmissionThreshold: 2, 17 | transmissionBlock: 0, 18 | } 19 | 20 | beforeAll(async () => { 21 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 22 | await initializeApi(endpoint) 23 | 24 | // Create a Test NFT 25 | const { test: testAccount } = await createTestPairs() 26 | const nEvent = await createNft("Test NFT Data", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 27 | TEST_DATA.nftId = nEvent.nftId 28 | }) 29 | 30 | afterAll(async () => { 31 | const { test: testAccount } = await createTestPairs() 32 | await removeTransmissionProtocol(TEST_DATA.nftId, testAccount, WaitUntil.BlockInclusion) 33 | }) 34 | 35 | describe("Testing transmission protocols extrinsics", (): void => { 36 | it("Should return the transmission protocol ProtocolSetEvent data", async () => { 37 | const { test: testAccount, dest: destAccount } = await createTestPairs() 38 | const lastBlockNumber = await getLastBlock() 39 | TEST_DATA.transmissionBlock = lastBlockNumber + 100 40 | const protocol = formatAtBlockWithResetProtocol("atBlockWithReset", TEST_DATA.transmissionBlock) 41 | const cancellation = formatProtocolCancellation("anytime") 42 | const tEvent = await setTransmissionProtocol( 43 | TEST_DATA.nftId, 44 | destAccount.address, 45 | protocol, 46 | cancellation, 47 | testAccount, 48 | WaitUntil.BlockInclusion, 49 | ) 50 | 51 | expect( 52 | tEvent?.nftId === TEST_DATA.nftId && 53 | tEvent.recipient == destAccount.address && 54 | ProtocolAction.AtBlockWithReset in tEvent.protocol && 55 | tEvent.protocol[ProtocolAction.AtBlockWithReset] == TEST_DATA.transmissionBlock && 56 | TransmissionCancellationAction.Anytime in tEvent.cancellation && 57 | tEvent.cancellation[TransmissionCancellationAction.Anytime] == null, 58 | ).toBe(true) 59 | }) 60 | 61 | it("Should return the transmission protocol TimerResetEvent data", async () => { 62 | const { test: testAccount } = await createTestPairs() 63 | TEST_DATA.transmissionBlock = TEST_DATA.transmissionBlock + 100 64 | const tEvent = await resetTransmissionProtocolTimer( 65 | TEST_DATA.nftId, 66 | TEST_DATA.transmissionBlock, 67 | testAccount, 68 | WaitUntil.BlockInclusion, 69 | ) 70 | 71 | expect(tEvent?.nftId === TEST_DATA.nftId && tEvent.newBlockNumber == TEST_DATA.transmissionBlock).toBe(true) 72 | }) 73 | 74 | it("Should return the NFT id of removed protocol", async () => { 75 | const { test: testAccount } = await createTestPairs() 76 | const tEvent = await removeTransmissionProtocol(TEST_DATA.nftId, testAccount, WaitUntil.BlockInclusion) 77 | 78 | expect(tEvent?.nftId === TEST_DATA.nftId).toBe(true) 79 | }) 80 | 81 | it("Should return the ConsentAddedEvent data: NFT id and user that gave his consent to a protocol", async () => { 82 | const { test: testAccount, dest: destAccount } = await createTestPairs() 83 | const consentList = [destAccount.address, `${process.env.SEED_TEST_FUNDS_PUBLIC}`] 84 | const protocol = formatOnConsentProtocol("onConsent", consentList, TEST_DATA.transmissionThreshold) 85 | const cancellation = formatProtocolCancellation("anytime") 86 | await setTransmissionProtocol( 87 | TEST_DATA.nftId, 88 | destAccount.address, 89 | protocol, 90 | cancellation, 91 | testAccount, 92 | WaitUntil.BlockInclusion, 93 | ) 94 | const tEvent = await addConsentToOnConsentProtocol(TEST_DATA.nftId, destAccount, WaitUntil.BlockInclusion) 95 | 96 | expect(tEvent?.nftId === TEST_DATA.nftId && tEvent.from == destAccount.address).toBe(true) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /src/nft/storage.ts: -------------------------------------------------------------------------------- 1 | import { hexToString } from "@polkadot/util" 2 | import BN from "bn.js" 3 | 4 | import { query } from "../blockchain" 5 | import { chainQuery, Errors, txPallets } from "../constants" 6 | import { CollectionData, NftData } from "./types" 7 | 8 | /** 9 | * @name nftMintFee 10 | * @summary Fee to mint an NFT (extra fee on top of the tx fees). 11 | * @returns NFT mint fee. 12 | */ 13 | export const getNftMintFee = async (): Promise => { 14 | const fee = await query(txPallets.nft, chainQuery.nftMintFee) 15 | return fee as any as BN 16 | } 17 | 18 | /** 19 | * @name getSecretNftMintFee 20 | * @summary Fee to mint a secret NFT (extra fee on top of the tx fees and basic nft). 21 | * @returns Secret NFT mint fee. 22 | */ 23 | export const getSecretNftMintFee = async (): Promise => { 24 | const fee = await query(txPallets.nft, chainQuery.secretNftMintFee) 25 | return fee as any as BN 26 | } 27 | 28 | /** 29 | * @name getCapsuleMintFee 30 | * @summary Fee to mint a Capsule. (extra fee on top of the tx fees). 31 | * @returns Capsule NFT mint fee. 32 | */ 33 | export const getCapsuleMintFee = async (): Promise => { 34 | const fee = await query(txPallets.nft, chainQuery.capsuleMintFee) 35 | return fee as any as BN 36 | } 37 | 38 | /** 39 | * @name getSecretNftOffchainData 40 | * @summary Get the secret offchain data of a Secret NFT. 41 | * @returns Secret NFT secret offchain data. 42 | */ 43 | export const getSecretNftOffchainData = async (nftId: number | string): Promise => { 44 | const secretOffchainData = await query(txPallets.nft, chainQuery.secretNftsOffchainData, [nftId]) 45 | return secretOffchainData.toHuman() as string 46 | } 47 | 48 | /** 49 | * @name getCapsuleOffchainData 50 | * @summary Get the capsule offchain data. 51 | * @returns The capsule offchain data. 52 | */ 53 | export const getCapsuleOffchainData = async (nftId: number | string): Promise => { 54 | const capsuleOffchainData = await query(txPallets.nft, chainQuery.capsuleOffchainData, [nftId]) 55 | return capsuleOffchainData.toHuman() as string 56 | } 57 | 58 | /** 59 | * @name getNextNftId 60 | * @summary Get the next NFT Id available. 61 | * @returns Number. 62 | */ 63 | export const getNextNftId = async (): Promise => { 64 | const id = await query(txPallets.nft, chainQuery.nextNFTId) 65 | return (id as any as BN).toNumber() 66 | } 67 | 68 | /** 69 | * @name getNextCollectionId 70 | * @summary Get the next collection Id available. 71 | * @returns Number. 72 | */ 73 | export const getNextCollectionId = async (): Promise => { 74 | const id = await query(txPallets.nft, chainQuery.nextCollectionId) 75 | return (id as any as BN).toNumber() 76 | } 77 | 78 | /** 79 | * @name getNftData 80 | * @summary Provides the data related to one NFT. 81 | * @param nftId The NFT id. 82 | * @returns A JSON object with the NFT data. ex:{owner, creator, offchainData, (...)} 83 | */ 84 | export const getNftData = async (nftId: number): Promise => { 85 | const data = await query(txPallets.nft, chainQuery.nfts, [nftId]) 86 | if (data.isEmpty == true) { 87 | return null 88 | } 89 | 90 | try { 91 | const result = data.toJSON() as NftData 92 | // The offchainData is an hexadecimal string, we convert it to a human readable string. 93 | if (result.offchainData) result.offchainData = hexToString(result.offchainData) 94 | return result 95 | } catch (error) { 96 | throw new Error(`${Errors.NFT_CONVERSION_ERROR}`) 97 | } 98 | } 99 | 100 | /** 101 | * @name getCollectionData 102 | * @summary Provides the data related to one NFT collection. ex:{owner, creator, offchainData, limit, isClosed(...)} 103 | * @param collectionId The collection id. 104 | * @returns A JSON object with data of a single NFT collection. 105 | */ 106 | export const getCollectionData = async (collectionId: number): Promise => { 107 | const data = await query(txPallets.nft, chainQuery.collections, [collectionId]) 108 | if (data.isEmpty == true) { 109 | return null 110 | } 111 | 112 | try { 113 | return data.toJSON() as any as CollectionData 114 | } catch (error) { 115 | throw new Error(`${Errors.COLLECTION_CONVERSION_ERROR}`) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/nft/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeApi } from "../blockchain" 2 | import { getCollectionOffchainDataLimit } from "./constants" 3 | import { 4 | getCapsuleMintFee, 5 | getCapsuleOffchainData, 6 | getCollectionData, 7 | getNextCollectionId, 8 | getNextNftId, 9 | getNftData, 10 | getNftMintFee, 11 | getSecretNftMintFee, 12 | getSecretNftOffchainData, 13 | } from "./storage" 14 | import { createTestPairs } from "../_misc/testingPairs" 15 | import { createCapsule, createCollection, createNft, createSecretNft } from "./extrinsics" 16 | import { WaitUntil } from "../constants" 17 | 18 | const TEST_DATA = { 19 | collectionId: 0, 20 | nftId: 0, 21 | secretNftId: 0, 22 | capsuleNftId: 0, 23 | } 24 | 25 | beforeAll(async () => { 26 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 27 | await initializeApi(endpoint) 28 | 29 | // Create some Test NFTs and Collections 30 | const { test: testAccount } = await createTestPairs() 31 | const cEvent = await createCollection("Collection Test", undefined, testAccount, WaitUntil.BlockInclusion) 32 | const nEvent = await createNft("Test NFT Data", 0, cEvent.collectionId, false, testAccount, WaitUntil.BlockInclusion) 33 | const secretNft = await createSecretNft( 34 | "Test NFT Data", 35 | "Test Secret NFT Data", 36 | 0, 37 | undefined, 38 | false, 39 | testAccount, 40 | WaitUntil.BlockInclusion, 41 | ) 42 | const capsuleNft = await createCapsule( 43 | "Test NFT offchainData", 44 | "Test Capsule NFT offchainData", 45 | 0, 46 | undefined, 47 | false, 48 | testAccount, 49 | WaitUntil.BlockInclusion, 50 | ) 51 | 52 | TEST_DATA.collectionId = cEvent.collectionId 53 | TEST_DATA.nftId = nEvent.nftId 54 | TEST_DATA.secretNftId = secretNft.nftId 55 | TEST_DATA.capsuleNftId = capsuleNft.nftId 56 | }) 57 | 58 | it("NFT Mint Fee storage should exist and it should not be null", async () => { 59 | const actual = await getNftMintFee() 60 | expect(actual != undefined).toBe(true) 61 | }) 62 | 63 | it("Secret NFT Mint Fee storage should exist and it should not be null", async () => { 64 | const actual = await getSecretNftMintFee() 65 | expect(actual != undefined).toBe(true) 66 | }) 67 | 68 | it("Capsule NFT Mint Fee storage should exist and it should not be null", async () => { 69 | const actual = await getCapsuleMintFee() 70 | expect(actual != undefined).toBe(true) 71 | }) 72 | 73 | it("Next NFT ID storage should exist and it should not be null", async () => { 74 | const actual = await getNextNftId() 75 | expect(actual != undefined).toBe(true) 76 | }) 77 | 78 | it("Next Collection ID storage should exist and it should not be null", async () => { 79 | const actual = await getNextCollectionId() 80 | expect(actual != undefined).toBe(true) 81 | }) 82 | 83 | it("Testing collection offchain data size limit to be 150", async () => { 84 | const actual = await getCollectionOffchainDataLimit() 85 | const expected = 150 86 | expect(actual).toEqual(expected) 87 | }) 88 | 89 | describe("Testing getting NFT data", (): void => { 90 | it("Should return null if an Invalid NFT ID is passed", async () => { 91 | const maybeNft = await getNftData(1000000) 92 | expect(maybeNft).toBeNull() 93 | }) 94 | it("Should return the NFT Data when the NFT ID exists", async () => { 95 | const maybeNft = await getNftData(TEST_DATA.nftId) 96 | expect(maybeNft != null).toBe(true) 97 | }) 98 | it("Should return secret offchain data when the NFT ID exists and is a Secret NFT", async () => { 99 | const actual = await getSecretNftOffchainData(TEST_DATA.secretNftId) 100 | expect(actual != undefined).toBe(true) 101 | }) 102 | it("Should return the capsule offchain data when the NFT ID exists and is a Capsule NFT", async () => { 103 | const actual = await getCapsuleOffchainData(TEST_DATA.capsuleNftId) 104 | expect(actual != undefined).toBe(true) 105 | }) 106 | }) 107 | 108 | describe("Testing Collection NFT data", (): void => { 109 | it("Should return null if an Invalid Collection ID is passed", async () => { 110 | const maybeCollection = await getCollectionData(1000000) 111 | expect(maybeCollection).toBeNull() 112 | }) 113 | it("Should return the NFT Data when the Collection ID exists", async () => { 114 | const maybeCollection = await getCollectionData(TEST_DATA.collectionId) 115 | expect(maybeCollection != null).toBe(true) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /src/balance/extrinsics.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js" 2 | import type { IKeyringPair } from "@polkadot/types/types" 3 | 4 | import { createTxHex, submitTxBlocking, numberToBalance, TransactionHashType } from "../blockchain" 5 | import { txActions, txPallets, WaitUntil } from "../constants" 6 | import { BalancesTransferEvent } from "../events" 7 | 8 | /** 9 | * @name balancesTransferTx 10 | * @summary Creates an unsigned unsubmitted Balance-Transfert Transaction Hash. 11 | * @param to Public address of the account to transfer the amount to. 12 | * @param amount Token amount to transfer. 13 | * @returns Unsigned unsubmitted Balance-Transfert Transaction Hash. The Hash is only valid for 5 minutes. 14 | */ 15 | export const balancesTransferTx = async (to: string, amount: number | BN): Promise => { 16 | const formattedAmount = typeof amount === "number" ? numberToBalance(amount) : amount 17 | return await createTxHex(txPallets.balances, txActions.transfer, [to, formattedAmount]) 18 | } 19 | 20 | /** 21 | * @name balancesTransfer 22 | * @summary Transfers some liquid free balance to another account. 23 | * @param to Public address of the account to transfer the amount to. 24 | * @param amount Token amount to transfer. 25 | * @param keyring Account that will sign the transaction. 26 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 27 | * @returns BalancesTransferEvent Blockchain event. 28 | */ 29 | export const balancesTransfer = async ( 30 | to: string, 31 | amount: number | BN, 32 | keyring: IKeyringPair, 33 | waitUntil: WaitUntil, 34 | ): Promise => { 35 | const tx = await balancesTransferTx(to, amount) 36 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 37 | return events.findEventOrThrow(BalancesTransferEvent) 38 | } 39 | 40 | /** 41 | * @name balancesTransferAllTx 42 | * @summary Creates an unsigned unsubmitted Balance-TransfertAll Transaction Hash. 43 | * @param to Public address of the account to transfer the amount to. 44 | * @param keepAlive Ensure that the transfer does not kill the account, it retains the Existential Deposit. 45 | * @returns Unsigned unsubmitted Balance-TransfertAll Transaction Hash. The Hash is only valid for 5 minutes. 46 | */ 47 | export const balancesTransferAllTx = async (to: string, keepAlive = true): Promise => { 48 | return await createTxHex(txPallets.balances, txActions.transferAll, [to, keepAlive]) 49 | } 50 | 51 | /** 52 | * @name balancesTransferAll 53 | * @summary Transfers the entire transferable balance from the caller account. 54 | * @param to Public address of the account to transfer the amount to. 55 | * @param keepAlive Ensure that the transfer does not kill the account, it retains the Existential Deposit. 56 | * @param keyring Account that will sign the transaction. 57 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 58 | * @returns BalancesTransferEvent Blockchain event. 59 | */ 60 | export const balancesTransferAll = async ( 61 | to: string, 62 | keepAlive = true, 63 | keyring: IKeyringPair, 64 | waitUntil: WaitUntil, 65 | ): Promise => { 66 | const tx = await balancesTransferAllTx(to, keepAlive) 67 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 68 | return events.findEventOrThrow(BalancesTransferEvent) 69 | } 70 | 71 | /** 72 | * @name balancesTransferKeepAliveTx 73 | * @summary Creates an unsigned unsubmitted Balance-TransfertKeepAlive Transaction Hash. 74 | * @param to Public address of the account to transfer the amount to. 75 | * @param amount Token amount to transfer. 76 | * @returns Unsigned unsubmitted Balance-TransfertKeepAlive Transaction Hash. The Hash is only valid for 5 minutes. 77 | */ 78 | export const balancesTransferKeepAliveTx = async (to: string, amount: number | BN): Promise => { 79 | const formattedAmount = typeof amount === "number" ? numberToBalance(amount) : amount 80 | return await createTxHex(txPallets.balances, txActions.transferKeepAlive, [to, formattedAmount]) 81 | } 82 | 83 | /** 84 | * @name balancesTransferKeepAlive 85 | * @summary Transfers some liquid free balance to another account with a check that the transfer will not kill the origin account. 86 | * @param to Public address of the account to transfer the amount to. 87 | * @param amount Token amount to transfer. 88 | * @param keyring Account that will sign the transaction. 89 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 90 | * @returns BalancesTransferEvent Blockchain event. 91 | */ 92 | export const balancesTransferKeepAlive = async ( 93 | to: string, 94 | amount: number | BN, 95 | keyring: IKeyringPair, 96 | waitUntil: WaitUntil, 97 | ): Promise => { 98 | const tx = await balancesTransferKeepAliveTx(to, amount) 99 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 100 | return events.findEventOrThrow(BalancesTransferEvent) 101 | } 102 | -------------------------------------------------------------------------------- /src/protocols/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getTransmissionAtBlockFee, 3 | getTransmissionAtBlockQueue, 4 | getTransmissionAtBlockWithResetFee, 5 | getTransmissionOnConsentAtBlockFee, 6 | getTransmissionOnConsentData, 7 | getTransmissionOnConsentFee, 8 | getTransmissions, 9 | } from "./storage" 10 | import { formatAtBlockProtocol, formatOnConsentAtBlockProtocol, formatProtocolCancellation } from "./utils" 11 | import { addConsentToOnConsentProtocol, removeTransmissionProtocol, setTransmissionProtocol } from "./extrinsics" 12 | import { initializeApi } from "../blockchain" 13 | import { createTestPairs } from "../_misc/testingPairs" 14 | import { WaitUntil } from "../constants" 15 | import { createNft } from "../nft" 16 | import { ProtocolAction, TransmissionCancellationAction } from "./enums" 17 | import { getLastBlock } from "../helpers/crypto" 18 | 19 | const TEST_DATA = { 20 | nftId: 0, 21 | transmissionThreshold: 2, 22 | transmissionBlock: 0, 23 | } 24 | 25 | beforeAll(async () => { 26 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 27 | await initializeApi(endpoint) 28 | 29 | // Create a Test NFT 30 | const { test: testAccount, dest: destAccount } = await createTestPairs() 31 | const nEvent = await createNft("Test NFT Data", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 32 | const lastBlockNumber = await getLastBlock() 33 | TEST_DATA.transmissionBlock = lastBlockNumber + 100 34 | const consentList = [destAccount.address, `${process.env.SEED_TEST_FUNDS_PUBLIC}`] 35 | const protocol = formatOnConsentAtBlockProtocol( 36 | "onConsentAtBlock", 37 | consentList, 38 | TEST_DATA.transmissionThreshold, 39 | TEST_DATA.transmissionBlock, 40 | ) 41 | const cancellation = formatProtocolCancellation("anytime") 42 | await setTransmissionProtocol( 43 | nEvent.nftId, 44 | destAccount.address, 45 | protocol, 46 | cancellation, 47 | testAccount, 48 | WaitUntil.BlockInclusion, 49 | ) 50 | TEST_DATA.nftId = nEvent.nftId 51 | }) 52 | 53 | afterAll(async () => { 54 | const { test: testAccount } = await createTestPairs() 55 | await removeTransmissionProtocol(TEST_DATA.nftId, testAccount, WaitUntil.BlockInclusion) 56 | }) 57 | 58 | describe("Testing to get transmission protocols fee", (): void => { 59 | it("Transmission protocol At Block fee should exist and it should not be null", async () => { 60 | const actual = await getTransmissionAtBlockFee() 61 | expect(actual != undefined).toBe(true) 62 | }) 63 | it("Transmission protocol At Block With Reset fee should exist and it should not be null", async () => { 64 | const actual = await getTransmissionAtBlockWithResetFee() 65 | expect(actual != undefined).toBe(true) 66 | }) 67 | it("Transmission protocol On Consent fee should exist and it should not be null", async () => { 68 | const actual = await getTransmissionOnConsentFee() 69 | expect(actual != undefined).toBe(true) 70 | }) 71 | it("Transmission protocol On Consent At Block fee should exist and it should not be null", async () => { 72 | const actual = await getTransmissionOnConsentAtBlockFee() 73 | expect(actual != undefined).toBe(true) 74 | }) 75 | }) 76 | 77 | describe("Testing to get transmission protocols data", (): void => { 78 | it("Should return the transmission protocol data", async () => { 79 | const { dest: destAccount } = await createTestPairs() 80 | const data = await getTransmissions(TEST_DATA.nftId) 81 | expect( 82 | data?.recipient == destAccount.address && 83 | ProtocolAction.OnConsentAtBlock in data.protocol && 84 | data.protocol[ProtocolAction.OnConsentAtBlock].consentList.length == 2 && 85 | data.protocol[ProtocolAction.OnConsentAtBlock].threshold == TEST_DATA.transmissionThreshold && 86 | data.protocol[ProtocolAction.OnConsentAtBlock].block == TEST_DATA.transmissionBlock && 87 | TransmissionCancellationAction.Anytime in data.cancellation && 88 | data.cancellation[TransmissionCancellationAction.Anytime] == null, 89 | ).toBe(true) 90 | }) 91 | it("Should return the list of address that gave their consent to the OnConsentAtBlock transmission protocol.", async () => { 92 | const { dest: destAccount } = await createTestPairs() 93 | await addConsentToOnConsentProtocol(TEST_DATA.nftId, destAccount, WaitUntil.BlockInclusion) 94 | const list = await getTransmissionOnConsentData(TEST_DATA.nftId) 95 | expect(list[0] == destAccount.address).toBe(true) 96 | }) 97 | it("Transmission protocol At Block fee should exist and it should not be null", async () => { 98 | const { test: testAccount, dest: destAccount } = await createTestPairs() 99 | await removeTransmissionProtocol(TEST_DATA.nftId, testAccount, WaitUntil.BlockInclusion) 100 | const protocol = formatAtBlockProtocol("atBlock", TEST_DATA.transmissionBlock) 101 | const cancellation = formatProtocolCancellation("anytime") 102 | await setTransmissionProtocol( 103 | TEST_DATA.nftId, 104 | destAccount.address, 105 | protocol, 106 | cancellation, 107 | testAccount, 108 | WaitUntil.BlockInclusion, 109 | ) 110 | const data = await getTransmissionAtBlockQueue() 111 | const idx = data.length - 1 112 | expect(data[idx].nftId == TEST_DATA.nftId && data[idx].blockNumber == TEST_DATA.transmissionBlock).toBe(true) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /src/rent/utils.ts: -------------------------------------------------------------------------------- 1 | import { numberToBalance } from "../blockchain" 2 | import { Errors } from "../constants" 3 | 4 | import { 5 | AcceptanceAction, 6 | CancellationFeeAction, 7 | DurationAction, 8 | RentFeeAction, 9 | SubscriptionActionDetails, 10 | } from "./enum" 11 | import { AcceptanceType, CancellationFeeType, DurationType, RentFeeType } from "./types" 12 | 13 | /** 14 | * @name formatDuration 15 | * @summary Returns an object representing a duration in either fixed or subscription format. 16 | * 17 | * @param type - The type of duration. Can be either 'fixed' or 'subscription'. 18 | * @param duration - The length of the contract duration in blocks. 19 | * @param maxDuration - (Optional) The maximum length of the contract subscription duration in blocks. Only applicable for subscriptions. 20 | * @param isChangeable - (Optional) A boolean indicating if the duration can be changed. Only applicable for subscriptions. 21 | * 22 | * @returns An object representing the duration of a contract. 23 | */ 24 | export const formatDuration = ( 25 | type: "fixed" | "subscription", 26 | duration: number, 27 | maxDuration?: number, 28 | isChangeable = false, 29 | ): DurationType => { 30 | if (type !== "fixed" && type !== "subscription") 31 | throw new Error("INCORRECT_TYPE: type has to be either 'fixed' or 'subscription'.") 32 | if (typeof duration !== "number") throw new Error("MUST_BE_A_NUMBER: duration must be a number.") 33 | 34 | return type === "fixed" 35 | ? { 36 | [DurationAction.Fixed]: duration, 37 | } 38 | : { 39 | [DurationAction.Subscription]: { 40 | [SubscriptionActionDetails.PeriodLength]: duration, 41 | [SubscriptionActionDetails.MaxDuration]: maxDuration ?? null, 42 | [SubscriptionActionDetails.IsChangeable]: isChangeable, 43 | }, 44 | } 45 | } 46 | 47 | /** 48 | * @name formatAcceptanceType 49 | * @summary Returns an object representing an acceptance type in either auto or manual format. 50 | * 51 | * @param type - The type of acceptance. Can be either 'auto' or 'manual'. 52 | * @param list - (Optional) A list of addresses. Only applicable for auto acceptance. 53 | * 54 | * @returns An object representing the acceptance type of a contract. 55 | */ 56 | export const formatAcceptanceType = (type: "auto" | "manual", list?: string[] | null): AcceptanceType => { 57 | if (type !== "auto" && type !== "manual") throw new Error("INCORRECT_TYPE: type has to be either 'auto' or 'manual'.") 58 | 59 | return type === "auto" 60 | ? { 61 | [AcceptanceAction.AutoAcceptance]: list ?? null, 62 | } 63 | : { 64 | [AcceptanceAction.ManualAcceptance]: list ?? null, 65 | } 66 | } 67 | 68 | /** 69 | * @name formatRentFee 70 | * @summary Returns an object representing a rent fee in either tokens or NFT format. 71 | * 72 | * @param type - The type of rent fee. Can be either 'tokens' or 'nft'. 73 | * @param value - The value of the rent fee. If type is 'tokens' value refers to a balance amount. If type is 'nft' value refers to the NFT id. 74 | * 75 | * @returns An object representing the rent fee of a contract. 76 | */ 77 | export const formatRentFee = (type: "tokens" | "nft", value: number): RentFeeType => { 78 | if (type !== "tokens" && type !== "nft") throw new Error("INCORRECT_TYPE: type has to be either 'tokens' or 'nft'.") 79 | if (typeof value !== "number") throw new Error("MUST_BE_A_NUMBER: value must be a number.") 80 | 81 | return type === "tokens" 82 | ? { 83 | [RentFeeAction.Tokens]: value, 84 | } 85 | : { 86 | [RentFeeAction.NFT]: value, 87 | } 88 | } 89 | 90 | /** 91 | * @name formatCancellationFee 92 | * @summary Returns an object representing a cancellation fee in either fixed, flexible or NFT format. 93 | * 94 | type: "fixed" | "flexible" | "nft" | "none", 95 | * @param type - The type of cancellation fee. Can be either 'fixed', 'flexible', 'nft' or 'none'. 96 | * @param value - The value of the rent fee. If type is 'fixed' or 'flexible' value refers to a balance amount. If type is 'nft' value refers to the NFT id. 97 | * 98 | * @returns An object representing the rent fee of a contract. 99 | */ 100 | export const formatCancellationFee = ( 101 | type: "fixed" | "flexible" | "nft" | "none", 102 | value?: number, 103 | ): CancellationFeeType => { 104 | if (type !== "fixed" && type !== "flexible" && type !== "nft" && type !== "none") 105 | throw new Error("INCORRECT_TYPE: type has to be either 'fixed', 'flexible', 'nft' or 'none'.") 106 | 107 | if (type === "none") return CancellationFeeAction.None 108 | if (value === undefined) throw new Error(`${Errors.VALUE_MUST_BE_DEFINED}`) 109 | switch (type) { 110 | case "fixed": 111 | return { 112 | [CancellationFeeAction.FixedTokens]: value, 113 | } 114 | case "flexible": 115 | return { 116 | [CancellationFeeAction.FlexibleTokens]: value, 117 | } 118 | case "nft": 119 | return { 120 | [CancellationFeeAction.NFT]: value, 121 | } 122 | default: 123 | return CancellationFeeAction.None 124 | } 125 | } 126 | 127 | /** 128 | * @name validateTransformContractFee 129 | * @summary Validates the type fee and format it accordingly. Numbers are formatted into BN. 130 | * @param fee The fee to format : It can only be a RentFeeType or CancellationFeeType. 131 | * @returns The formatted fee. 132 | */ 133 | export const validateTransformContractFee = ( 134 | fee: T, 135 | ): RentFeeType | CancellationFeeType => { 136 | if (typeof fee === "object") { 137 | if ("tokens" in fee && typeof fee.tokens === "number") { 138 | const tokensFee = numberToBalance(fee.tokens) 139 | fee.tokens = tokensFee 140 | } 141 | if ("fixedTokens" in fee && typeof fee.fixedTokens === "number") { 142 | const tokensFee = numberToBalance(fee.fixedTokens) 143 | fee.fixedTokens = tokensFee 144 | } 145 | if ("flexibleTokens" in fee && typeof fee.flexibleTokens === "number") { 146 | const tokensFee = numberToBalance(fee.flexibleTokens) 147 | fee.flexibleTokens = tokensFee 148 | } 149 | } 150 | return fee 151 | } 152 | -------------------------------------------------------------------------------- /src/rent/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { acceptRentOffer, createContract, makeRentOffer, rent, revokeContract } from "./extrinsics" 2 | import { AcceptanceAction, CancellationFeeAction, DurationAction, RentFeeAction } from "./enum" 3 | import { getRentalContractData, getRentalOffers, getRentingQueues } from "./storage" 4 | import { formatAcceptanceType, formatCancellationFee, formatDuration, formatRentFee } from "./utils" 5 | 6 | import { initializeApi, numberToBalance } from "../blockchain" 7 | import { WaitUntil } from "../constants" 8 | import { createNft } from "../nft" 9 | import { createTestPairs } from "../_misc/testingPairs" 10 | 11 | const TEST_DATA = { 12 | nftId: 0, 13 | contractCreationBlockId: 0, 14 | } 15 | 16 | beforeAll(async () => { 17 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 18 | await initializeApi(endpoint) 19 | 20 | // Create some Test NFT and a RentContract 21 | const { test: testAccount } = await createTestPairs() 22 | const nEvent = await createNft("TEST_NFT_DATA", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 23 | TEST_DATA.nftId = nEvent.nftId 24 | const duration = formatDuration("fixed", 1000) 25 | const acceptanceType = formatAcceptanceType("manual") 26 | const rentFee = formatRentFee("tokens", 1) 27 | const renterCancellationFee = formatCancellationFee("flexible", 1) 28 | const renteeCancellationFee = formatCancellationFee("none") 29 | const contractEvent = await createContract( 30 | TEST_DATA.nftId, 31 | duration, 32 | acceptanceType, 33 | false, 34 | rentFee, 35 | renterCancellationFee, 36 | renteeCancellationFee, 37 | testAccount, 38 | WaitUntil.BlockInclusion, 39 | ) 40 | TEST_DATA.contractCreationBlockId = contractEvent.creationBlockId 41 | }) 42 | 43 | describe("Testing contracts in queue and getting contract data", (): void => { 44 | it("Should return the rent contract data when an NFT ID with a rent contract exists", async () => { 45 | const { test: testAccount } = await createTestPairs() 46 | const contract = await getRentalContractData(TEST_DATA.nftId) 47 | const rentFee = numberToBalance(1).toString() 48 | const cancellationFee = numberToBalance(1).toString() 49 | 50 | expect( 51 | contract?.creationBlock == TEST_DATA.contractCreationBlockId && 52 | contract?.startBlock == null && 53 | contract?.startBlockDate == null && 54 | contract?.renter == testAccount.address && 55 | contract.rentee == null && 56 | DurationAction.Fixed in contract.duration && 57 | contract.duration[DurationAction.Fixed] == 1000 && 58 | contract.acceptanceType === AcceptanceAction.ManualAcceptance && 59 | contract.acceptanceList.length == 0 && 60 | contract.rentFeeType === RentFeeAction.Tokens && 61 | contract.rentFee === rentFee && 62 | contract.rentFeeRounded === 1 && 63 | contract.renterCanRevoke == false && 64 | contract.renterCancellationFeeType === CancellationFeeAction.FlexibleTokens && 65 | contract.renterCancellationFee === cancellationFee && 66 | contract.renterCancellationFeeRounded === 1 && 67 | contract.renteeCancellationFeeType === CancellationFeeAction.None && 68 | contract.renteeCancellationFee === null && 69 | contract.renteeCancellationFeeRounded === null, 70 | ).toBe(true) 71 | }) 72 | 73 | it("Should return nftId and expirationBlockId of the first available queue", async () => { 74 | const { availableQueue } = await getRentingQueues() 75 | const filteredContract = availableQueue.filter((x) => x.nftId === TEST_DATA.nftId) 76 | expect(filteredContract[0].nftId >= TEST_DATA.nftId && filteredContract[0].expirationBlockId >= 0).toBe(true) 77 | }) 78 | 79 | it("Should return the address of the offer made on an NFT", async () => { 80 | const { dest: destAccount } = await createTestPairs() 81 | const { rentee } = await makeRentOffer( 82 | TEST_DATA.nftId, 83 | TEST_DATA.contractCreationBlockId, 84 | destAccount, 85 | WaitUntil.BlockInclusion, 86 | ) 87 | const offer = await getRentalOffers(TEST_DATA.nftId) 88 | expect(rentee === destAccount.address && offer[0] === rentee).toBe(true) 89 | }) 90 | 91 | it("Should return the nftId and endingBlockId of the first fixed queue running contract", async () => { 92 | const { test: testAccount, dest: destAccount } = await createTestPairs() 93 | await acceptRentOffer(TEST_DATA.nftId, destAccount.address, testAccount, WaitUntil.BlockInclusion) 94 | const { fixedQueue } = await getRentingQueues() 95 | const filteredContract = fixedQueue.filter((x) => x.nftId === TEST_DATA.nftId) 96 | expect(filteredContract[0].nftId >= TEST_DATA.nftId && filteredContract[0].endingBlockId >= 0).toBe(true) 97 | }) 98 | 99 | it("Should return null if an invalid rental contract ID (the nftID) is passed", async () => { 100 | const { dest: destAccount } = await createTestPairs() 101 | await revokeContract(TEST_DATA.nftId, destAccount, WaitUntil.BlockInclusion) 102 | const maybeRentContract = await getRentalContractData(TEST_DATA.nftId) 103 | expect(maybeRentContract).toBeNull() 104 | }) 105 | 106 | it("Should return the nftId and renewalOrEndBlockId of the first subscription queue running contract", async () => { 107 | const { test: testAccount, dest: destAccount } = await createTestPairs() 108 | const duration = formatDuration("subscription", 5, 10) 109 | const acceptanceType = formatAcceptanceType("auto") 110 | const rentFee = formatRentFee("tokens", 1) 111 | const renterCancellationFee = formatCancellationFee("none") 112 | const renteeCancellationFee = formatCancellationFee("none") 113 | const contract = await createContract( 114 | TEST_DATA.nftId, 115 | duration, 116 | acceptanceType, 117 | false, 118 | rentFee, 119 | renterCancellationFee, 120 | renteeCancellationFee, 121 | testAccount, 122 | WaitUntil.BlockInclusion, 123 | ) 124 | await rent(TEST_DATA.nftId, contract.creationBlockId, destAccount, WaitUntil.BlockInclusion) 125 | const { subscriptionQueue } = await getRentingQueues() 126 | const filteredContract = subscriptionQueue.filter((x) => x.nftId === TEST_DATA.nftId) 127 | const periodLength = 128 | DurationAction.Subscription in contract.duration && contract.duration[DurationAction.Subscription].periodLength 129 | expect( 130 | filteredContract[0].nftId >= TEST_DATA.nftId && 131 | periodLength && 132 | filteredContract[0].renewalOrEndBlockId >= periodLength, 133 | ).toBe(true) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /src/protocols/utils.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolAction, TransmissionCancellationAction } from "./enums" 2 | import { 3 | TransmissionAtBlock, 4 | TransmissionAtBlockWithReset, 5 | TransmissionCancellation, 6 | TransmissionOnConsent, 7 | TransmissionOnConsentAtBlock, 8 | } from "./types" 9 | import { isValidAddress } from "../blockchain" 10 | 11 | /** 12 | * @name formatAtBlockProtocol 13 | * @summary Returns an object according to the atBlock transmission protocol format. 14 | * @param protocol The protocol (string) : "atBlock". 15 | * @param executionBlock The block number to execute the atBlock transmission protocol. 16 | * @returns An object representing the atBlock transmission protocol. 17 | */ 18 | export const formatAtBlockProtocol = (protocol: "atBlock", executionBlock: number): TransmissionAtBlock => { 19 | if (protocol !== ProtocolAction.AtBlock) throw new Error("INCORRECT_PROTOCOL: expected 'atBlock'.") 20 | if (typeof executionBlock !== "number") throw new Error("MUST_BE_A_NUMBER: executionBlock must be a number.") 21 | return { [ProtocolAction.AtBlock]: executionBlock } 22 | } 23 | 24 | /** 25 | * @name formatAtBlockWithResetProtocol 26 | * @summary Returns an object according to the atBlockWithReset transmission protocol format. 27 | * @param protocol The protocol (string) : "BlockWithReset". 28 | * @param executionBlockWithReset The block number to execute the atBlockWithReset transmission protocol. It can be updated later by user. 29 | * @returns An object representing the atBlockWithReset transmission protocol. 30 | */ 31 | export const formatAtBlockWithResetProtocol = ( 32 | protocol: "atBlockWithReset", 33 | executionBlockWithReset: number, 34 | ): TransmissionAtBlockWithReset => { 35 | if (protocol !== ProtocolAction.AtBlockWithReset) throw new Error("INCORRECT_PROTOCOL: expected 'atBlockWithReset'.") 36 | if (typeof executionBlockWithReset !== "number") 37 | throw new Error("MUST_BE_A_NUMBER: executionBlockWithReset must be a number.") 38 | return { [ProtocolAction.AtBlockWithReset]: executionBlockWithReset } 39 | } 40 | 41 | /** 42 | * @name formatOnConsentProtocol 43 | * @summary Returns an object according to the OnConsent transmission protocol format. 44 | * @param protocol The protocol (string) : "OnConsent". 45 | * @param consentList An array of account address that need to consent the protocol. 46 | * @param threshold The minimum number of consent to valid the protocol execution. 47 | * @returns An object representing the onConsent transmission protocol. 48 | */ 49 | export const formatOnConsentProtocol = ( 50 | protocol: "onConsent", 51 | consentList: string[], 52 | threshold: number, 53 | ): TransmissionOnConsent => { 54 | if (protocol !== ProtocolAction.OnConsent) throw new Error("INCORRECT_PROTOCOL: expected 'onConsent'.") 55 | consentList.map((address) => { 56 | if (typeof address !== "string" && !isValidAddress(address)) 57 | throw new Error("MUST_BE_A_STRING: consentList must only contains only valid address.") 58 | }) 59 | if (typeof threshold !== "number") throw new Error("MUST_BE_A_NUMBER: threshold must be a number.") 60 | return { 61 | [ProtocolAction.OnConsent]: { 62 | consentList, 63 | threshold, 64 | }, 65 | } 66 | } 67 | 68 | /** 69 | * @name formatOnConsentAtBlockProtocol 70 | * @summary Returns an object according to the onConsentAtBlock transmission protocol format. 71 | * @param protocol The protocol (string) : "onConsentAtBlock". 72 | * @param consentList An array of account address that need to consent the protocol. 73 | * @param threshold The minimum number of consent to valid the protocol execution. 74 | * @param block The block number before which each user consent is expected. 75 | * @returns An object representing the onConsentAtBlock transmission protocol. 76 | */ 77 | export const formatOnConsentAtBlockProtocol = ( 78 | protocol: "onConsentAtBlock", 79 | consentList: string[], 80 | threshold: number, 81 | block: number, 82 | ): TransmissionOnConsentAtBlock => { 83 | if (protocol !== ProtocolAction.OnConsentAtBlock) throw new Error("INCORRECT_PROTOCOL: expected 'onConsentAtBlock'.") 84 | consentList.map((address) => { 85 | if (typeof address !== "string" && !isValidAddress(address)) 86 | throw new Error("MUST_BE_A_STRING: consentList must only contains only valid address.") 87 | }) 88 | if (typeof threshold !== "number" && typeof block !== "number") 89 | throw new Error("MUST_BE_A_NUMBER: threshold and block must be numbers.") 90 | return { 91 | [ProtocolAction.OnConsentAtBlock]: { 92 | consentList, 93 | threshold, 94 | block, 95 | }, 96 | } 97 | } 98 | 99 | /** 100 | * @name formatProtocolCancellation 101 | * @summary Returns an object according to the cancellation kind required. 102 | * @param cancellation The cancellation kind (string) : "anytime", "none" or "untilBlock". 103 | * @param UntilBlock The block number before which user cancellation is available. Can only be set for "untilBlock" cancellation. 104 | * @returns An object representing the cancellation of the transmission protocol. 105 | */ 106 | export const formatProtocolCancellation = ( 107 | cancellation: "anytime" | "none" | "untilBlock", 108 | UntilBlock?: number, 109 | ): TransmissionCancellation => { 110 | if ( 111 | cancellation !== TransmissionCancellationAction.Anytime && 112 | cancellation !== TransmissionCancellationAction.None && 113 | cancellation !== TransmissionCancellationAction.UntilBlock 114 | ) 115 | throw new Error("INCORRECT_CANCELLATION: cancellation must be either 'anytime', 'none' or 'untilBlock'.") 116 | if ( 117 | UntilBlock && 118 | (cancellation == TransmissionCancellationAction.None || cancellation == TransmissionCancellationAction.Anytime) 119 | ) 120 | throw new Error("INCORRECT_CANCELLATION: untilBlock number can't be set for 'anytime' or 'none' cancellation.") 121 | if (cancellation == TransmissionCancellationAction.UntilBlock && !UntilBlock) 122 | throw new Error("MISSING_DATA: untilBlock cancellation must have 'UntilBlock' param define as number.") 123 | if (UntilBlock && typeof UntilBlock !== "number") throw new Error("MUST_BE_A_NUMBER: UntilBlock must be a number.") 124 | 125 | return cancellation === TransmissionCancellationAction.Anytime 126 | ? { [TransmissionCancellationAction.Anytime]: null } 127 | : cancellation === TransmissionCancellationAction.UntilBlock && UntilBlock 128 | ? { [TransmissionCancellationAction.UntilBlock]: UntilBlock } 129 | : { [TransmissionCancellationAction.None]: null } 130 | } 131 | -------------------------------------------------------------------------------- /src/auction/extrinsics.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestPairs } from "../_misc/testingPairs" 2 | import { initializeApi, numberToBalance, query } from "../blockchain" 3 | import { chainQuery, txPallets, WaitUntil } from "../constants" 4 | import { createNft } from "../nft" 5 | import { createMarketplace } from "../marketplace" 6 | import { MarketplaceKind } from "../marketplace/enum" 7 | 8 | import { getMinAuctionDuration } from "./constants" 9 | import { addBid, buyItNow, cancelAuction, createAuction, removeBid } from "./extrinsics" 10 | 11 | const TEST_DATA = { 12 | marketplaceId: 0, 13 | nftId: 0, 14 | startBlock: 0, 15 | endBlock: 0, 16 | } 17 | 18 | beforeAll(async () => { 19 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 20 | await initializeApi(endpoint) 21 | 22 | // Create some Test NFT, Marketplace and Auction 23 | const { test: testAccount } = await createTestPairs() 24 | const nEvent = await createNft("Test Auctioned NFT", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 25 | const mEvent = await createMarketplace(MarketplaceKind.Public, testAccount, WaitUntil.BlockInclusion) 26 | TEST_DATA.nftId = nEvent.nftId 27 | TEST_DATA.marketplaceId = mEvent.marketplaceId 28 | }) 29 | 30 | describe("Testing to create an auction & buy NFT directly with buyItNow", (): void => { 31 | it("Testing to create an auction", async (): Promise => { 32 | const { test: testAccount } = await createTestPairs() 33 | const currentBlock = await query(txPallets.system, chainQuery.number) 34 | const auctionMinDuration = getMinAuctionDuration() 35 | const auctionStartBlock = Number.parseInt(currentBlock.toString()) + 1 36 | const auctionEndBlock = auctionStartBlock + auctionMinDuration 37 | const startPrice = numberToBalance(1) 38 | const buyItPrice = numberToBalance(10) 39 | const aEvent = await createAuction( 40 | TEST_DATA.nftId, 41 | TEST_DATA.marketplaceId, 42 | auctionStartBlock, 43 | auctionEndBlock, 44 | startPrice, 45 | buyItPrice, 46 | testAccount, 47 | WaitUntil.BlockInclusion, 48 | ) 49 | TEST_DATA.startBlock = auctionStartBlock 50 | TEST_DATA.endBlock = auctionEndBlock 51 | 52 | expect( 53 | aEvent.method === "AuctionCreated" && 54 | aEvent.creator === testAccount.address && 55 | aEvent.nftId === TEST_DATA.nftId && 56 | aEvent.marketplaceId === TEST_DATA.marketplaceId && 57 | aEvent.startPrice === startPrice.toString() && 58 | aEvent.startPriceRounded === 1 && 59 | aEvent.buyItPrice === buyItPrice.toString() && 60 | aEvent.buyItPriceRounded === 10 && 61 | aEvent.startBlock === TEST_DATA.startBlock && 62 | aEvent.endBlock === TEST_DATA.endBlock, 63 | ).toBe(true) 64 | }) 65 | 66 | it("Testing to buy an Auctioned NFT directly with buyItNow", async (): Promise => { 67 | const { dest: destAccount } = await createTestPairs() 68 | const buyItPrice = 10 69 | const buyItPriceBN = numberToBalance(10) 70 | const zeroBN = numberToBalance(0) 71 | const aEvent = await buyItNow(TEST_DATA.nftId, buyItPriceBN, destAccount, WaitUntil.BlockInclusion) 72 | 73 | expect( 74 | aEvent.method === "AuctionCompleted" && 75 | aEvent.newOwner === destAccount.address && 76 | aEvent.nftId === TEST_DATA.nftId && 77 | aEvent.amount === buyItPriceBN.toString() && 78 | aEvent.amountRounded === buyItPrice && 79 | aEvent.marketplaceCut === zeroBN.toString() && 80 | aEvent.marketplaceCutRounded === 0 && 81 | aEvent.royaltyCut === zeroBN.toString() && 82 | aEvent.royaltyCutRounded === 0, 83 | ).toBe(true) 84 | }) 85 | }) 86 | 87 | it("Testing to cancel an auction", async (): Promise => { 88 | const { test: testAccount } = await createTestPairs() 89 | const nEvent = await createNft("Test Auctioned NFT", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 90 | const currentBlock = await query(txPallets.system, chainQuery.number) 91 | const auctionMinDuration = getMinAuctionDuration() 92 | const auctionStartBlock = Number.parseInt(currentBlock.toString()) + 10 93 | const auctionEndBlock = auctionStartBlock + auctionMinDuration 94 | const startPrice = numberToBalance(1) 95 | const buyItPrice = numberToBalance(10) 96 | await createAuction( 97 | nEvent.nftId, 98 | TEST_DATA.marketplaceId, 99 | auctionStartBlock, 100 | auctionEndBlock, 101 | startPrice, 102 | buyItPrice, 103 | testAccount, 104 | WaitUntil.BlockInclusion, 105 | ) 106 | const aEvent = await cancelAuction(nEvent.nftId, testAccount, WaitUntil.BlockInclusion) 107 | 108 | expect(aEvent.method === "AuctionCancelled" && aEvent.nftId === nEvent.nftId).toBe(true) 109 | }) 110 | 111 | describe("Testing to create an auction & add/update/remove bids", (): void => { 112 | it("Testing to create an auction & add a bid", async (): Promise => { 113 | const { test: testAccount, dest: destAccount } = await createTestPairs() 114 | const nEvent = await createNft("Test Auctioned NFT", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 115 | const currentBlock = await query(txPallets.system, chainQuery.number) 116 | const auctionMinDuration = getMinAuctionDuration() 117 | const auctionStartBlock = Number.parseInt(currentBlock.toString()) + 1 118 | const auctionEndBlock = auctionStartBlock + auctionMinDuration * 2 119 | const startPrice = numberToBalance(1) 120 | const buyItPrice = numberToBalance(10) 121 | await createAuction( 122 | nEvent.nftId, 123 | TEST_DATA.marketplaceId, 124 | auctionStartBlock, 125 | auctionEndBlock, 126 | startPrice, 127 | buyItPrice, 128 | testAccount, 129 | WaitUntil.BlockInclusion, 130 | ) 131 | TEST_DATA.nftId = nEvent.nftId 132 | 133 | const bidAmount = 10 134 | const bidAmountBN = numberToBalance(10) 135 | const aEvent = await addBid(nEvent.nftId, bidAmount, destAccount, WaitUntil.BlockInclusion) 136 | 137 | expect( 138 | aEvent.method === "BidAdded" && 139 | aEvent.nftId === nEvent.nftId && 140 | aEvent.amount === bidAmountBN.toString() && 141 | aEvent.amountRounded === bidAmount && 142 | aEvent.bidder === destAccount.address, 143 | ).toBe(true) 144 | }) 145 | 146 | it("Testing to remove a bid", async (): Promise => { 147 | const { dest: destAccount } = await createTestPairs() 148 | const bidAmount = 10 149 | const bidAmountBN = numberToBalance(bidAmount) 150 | const aEvent = await removeBid(TEST_DATA.nftId, destAccount, WaitUntil.BlockInclusion) 151 | 152 | expect( 153 | aEvent.method === "BidRemoved" && 154 | aEvent.nftId === TEST_DATA.nftId && 155 | aEvent.amount === bidAmountBN.toString() && 156 | aEvent.amountRounded === bidAmount && 157 | aEvent.bidder === destAccount.address, 158 | ).toBe(true) 159 | }) 160 | }) 161 | -------------------------------------------------------------------------------- /src/marketplace/utils.ts: -------------------------------------------------------------------------------- 1 | import { AccountListType, CollectionListType, CommissionFeeType, ListingFeeType, OffchainDataType } from "./types" 2 | 3 | import { formatPermill } from "../helpers/utils" 4 | import { numberToBalance } from "../blockchain" 5 | import { Errors } from "../constants" 6 | import { MarketplaceConfigAction, MarketplaceConfigFeeType } from "./enum" 7 | 8 | /** 9 | * @name convertMarketplaceFee 10 | * @summary Checks the type fee and format it accordingly. Numbers are formatted into BN. Percentages are formatted in Permill. 11 | * @param fee The fee to format : It can only be an CommissionFeeType or ListingFeeType. 12 | * @returns The formatted fee. 13 | */ 14 | export const convertMarketplaceFee = async (fee: CommissionFeeType | ListingFeeType) => { 15 | if (typeof fee === "object") { 16 | if (typeof fee.set.flat === "number") { 17 | const flatFee = numberToBalance(fee.set.flat) 18 | fee.set.flat = flatFee 19 | } 20 | if (fee.set.percentage) { 21 | const percentageFee = formatPermill(fee.set.percentage) 22 | fee.set.percentage = percentageFee 23 | } 24 | } 25 | return fee 26 | } 27 | 28 | /** 29 | * @name formatMarketplaceFee 30 | * @summary Returns an object representing either the marketplace commission or listing fee in either in Flat or Percentage format. 31 | * 32 | * @param action - The type of Action. Can be either "Noop" (No Operation: to keep it as it is), "Remove" or "set". 33 | * @param feeType - The type of fee. Can be either "percentage" or "flat", 34 | * @param value - The value of the fee. If type is 'Percentage' value refers to a decimal number in range [0, 100]. If type is 'Flat' value refers to a balance amount in a number. Default is 0. 35 | * 36 | * @returns An object representing either the marketplace commission or listing fee. 37 | */ 38 | export const formatMarketplaceFee = ( 39 | action: "Noop" | "Remove" | "set", 40 | feeType?: "percentage" | "flat", 41 | value?: number, 42 | ): CommissionFeeType => { 43 | if (action !== "Noop" && action !== "Remove" && action !== "set") 44 | throw new Error("INCORRECT_ACTION: action has to be either 'Noop', 'Remove', 'set'.") 45 | if (feeType && feeType !== "percentage" && feeType !== "flat") 46 | throw new Error("INCORRECT_FEE_TYPE: feeType has to be either 'percentage' or 'flat'.") 47 | switch (action) { 48 | case "Noop": 49 | return MarketplaceConfigAction.Noop 50 | case "Remove": 51 | return MarketplaceConfigAction.Remove 52 | case "set": { 53 | if (value === undefined || feeType === undefined) throw new Error(`${Errors.VALUE_MUST_BE_DEFINED}`) 54 | if (feeType && feeType === "percentage") { 55 | return { 56 | [MarketplaceConfigAction.Set]: { [MarketplaceConfigFeeType.Percentage]: formatPermill(value) }, 57 | } 58 | } else { 59 | return { 60 | [MarketplaceConfigAction.Set]: { [MarketplaceConfigFeeType.Flat]: numberToBalance(value) }, 61 | } 62 | } 63 | } 64 | default: 65 | return MarketplaceConfigAction.Noop 66 | } 67 | } 68 | 69 | /** 70 | * @name formatMarketplaceAccountList 71 | * @summary Returns an object representing a list of accounts : if the marketplace kind is private, it allows these accounts to sell NFT. If the marketplace kind is public, it bans these accounts from selling NFT. 72 | * 73 | * @param action - The type of Action. Can be either "Noop" (No Operation: to keep it as it is), "Remove" or "set". 74 | * @param value - An array of addresses (string) to add to the list. 75 | * 76 | * @returns An object representing either the whitelisted or banned accounts. 77 | */ 78 | export const formatMarketplaceAccountList = (action: "Noop" | "Remove" | "set", value?: string[]): AccountListType => { 79 | if (action !== "Noop" && action !== "Remove" && action !== "set") 80 | throw new Error("INCORRECT_ACTION: action has to be either 'Noop', 'Remove', 'set'.") 81 | switch (action) { 82 | case "Noop": 83 | return MarketplaceConfigAction.Noop 84 | case "Remove": 85 | return MarketplaceConfigAction.Remove 86 | case "set": { 87 | if (value === undefined) throw new Error(`${Errors.VALUE_MUST_BE_DEFINED}`) 88 | return { 89 | [MarketplaceConfigAction.Set]: value, 90 | } 91 | } 92 | default: 93 | return MarketplaceConfigAction.Noop 94 | } 95 | } 96 | 97 | /** 98 | * @name formatMarketplaceOffchainData 99 | * @summary Returns the off-chain related marketplace metadata. Can be an IPFS Hash, an URL or plain text. 100 | * 101 | * @param action - The type of Action. Can be either "Noop" (No Operation: to keep it as it is), "Remove" or "set". 102 | * @param value - The marketplkace offchain metadata : a string 103 | * 104 | * @returns An object representing either the marketplace offchain metadata. 105 | */ 106 | export const formatMarketplaceOffchainData = (action: "Noop" | "Remove" | "set", value?: string): OffchainDataType => { 107 | if (action !== "Noop" && action !== "Remove" && action !== "set") 108 | throw new Error("INCORRECT_ACTION: action has to be either 'Noop', 'Remove', 'set'.") 109 | switch (action) { 110 | case "Noop": 111 | return MarketplaceConfigAction.Noop 112 | case "Remove": 113 | return MarketplaceConfigAction.Remove 114 | case "set": { 115 | if (value === undefined) throw new Error(`${Errors.VALUE_MUST_BE_DEFINED}`) 116 | return { 117 | [MarketplaceConfigAction.Set]: value, 118 | } 119 | } 120 | default: 121 | return MarketplaceConfigAction.Noop 122 | } 123 | } 124 | 125 | /** 126 | * @name formatMarketplaceCollectionList 127 | * @summary Returns an object representing a list of collection of NFT : if the marketplace kind is private, it allows these collection to be listed. If the marketplace kind is public, it bans these collection of NFT from listing. 128 | * 129 | * @param action - The type of Action. Can be either "Noop" (No Operation: to keep it as it is), "Remove" or "set". 130 | * @param value - An array of Collection id (number) to add to the list. 131 | * 132 | * @returns An object representing either the whitelisted or banned collection Id. 133 | */ 134 | export const formatMarketplaceCollectionList = ( 135 | action: "Noop" | "Remove" | "set", 136 | value?: number[], 137 | ): CollectionListType => { 138 | if (action !== "Noop" && action !== "Remove" && action !== "set") 139 | throw new Error("INCORRECT_ACTION: action has to be either 'Noop', 'Remove', 'set'.") 140 | switch (action) { 141 | case "Noop": 142 | return MarketplaceConfigAction.Noop 143 | case "Remove": 144 | return MarketplaceConfigAction.Remove 145 | case "set": { 146 | if (value === undefined) throw new Error(`${Errors.VALUE_MUST_BE_DEFINED}`) 147 | return { 148 | [MarketplaceConfigAction.Set]: value, 149 | } 150 | } 151 | default: 152 | return MarketplaceConfigAction.Noop 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/protocols/extrinsics.ts: -------------------------------------------------------------------------------- 1 | import { IKeyringPair } from "@polkadot/types/types" 2 | import { ConsentAddedEvent, ProtocolRemovedEvent, ProtocolSetEvent, TimerResetEvent } from "../events" 3 | 4 | import { createTxHex, submitTxBlocking, TransactionHashType } from "../blockchain" 5 | import { txActions, txPallets, WaitUntil } from "../constants" 6 | import { Protocols, TransmissionCancellation } from "./types" 7 | 8 | /** 9 | * @name setTransmissionProtocolTx 10 | * @summary Creates an unsigned unsubmittedSet-Transmission-Protocol Transaction Hash. 11 | * @param nftId The NFT Id to add transmission protocol. 12 | * @param recipient The destination account. 13 | * @param protocol The transmission protocol to execute. 14 | * @param protocolCancellation the cancellation period of the transmission protocol. 15 | * @returns Unsigned unsubmitted Set-Transmission-Protocol Transaction Hash. The Hash is only valid for 5 minutes. 16 | */ 17 | export const setTransmissionProtocolTx = async ( 18 | nftId: number, 19 | recipient: string, 20 | protocol: Protocols, 21 | protocolCancellation: TransmissionCancellation, 22 | ): Promise => { 23 | return await createTxHex(txPallets.transmissionProtocols, txActions.setTransmissionProtocol, [ 24 | nftId, 25 | recipient, 26 | protocol, 27 | protocolCancellation, 28 | ]) 29 | } 30 | 31 | /** 32 | * @name setTransmissionProtocol 33 | * @summary Adds a transmission protocol to any type of NFT. 34 | * @param nftId The NFT Id to add transmission protocol. 35 | * @param recipient The destination account. 36 | * @param protocol The transmission protocol to execute. 37 | * @param protocolCancellation the cancellation period of the transmission protocol. 38 | * @param keyring Account that will sign the transaction. 39 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 40 | * @returns ProtocolSetEvent Blockchain event. 41 | */ 42 | export const setTransmissionProtocol = async ( 43 | nftId: number, 44 | recipient: string, 45 | protocol: Protocols, 46 | protocolCancellation: TransmissionCancellation, 47 | keyring: IKeyringPair, 48 | waitUntil: WaitUntil, 49 | ): Promise => { 50 | const tx = await setTransmissionProtocolTx(nftId, recipient, protocol, protocolCancellation) 51 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 52 | return events.findEventOrThrow(ProtocolSetEvent) 53 | } 54 | 55 | /** 56 | * @name removeTransmissionProtocolTx 57 | * @summary Creates an unsigned unsubmitted Remove-Transmission-Protocol Transaction Hash for a transmission protocol. 58 | * @param nftId The NFT Id to remove the transmission protocol. 59 | * @returns Unsigned unsubmitted Remove-Transmission-Protocol Transaction Hash. The Hash is only valid for 5 minutes. 60 | */ 61 | export const removeTransmissionProtocolTx = async (nftId: number): Promise => { 62 | return await createTxHex(txPallets.transmissionProtocols, txActions.removeTransmissionProtocol, [nftId]) 63 | } 64 | 65 | /** 66 | * @name removeTransmissionProtocol 67 | * @summary Removes a transmission protocol from an NFT. 68 | * @param nftId The NFT Id to remove the transmission protocol. 69 | * @param keyring Account that will sign the transaction. 70 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 71 | * @returns ProtocolRemovedEvent Blockchain event. 72 | */ 73 | export const removeTransmissionProtocol = async ( 74 | nftId: number, 75 | keyring: IKeyringPair, 76 | waitUntil: WaitUntil, 77 | ): Promise => { 78 | const tx = await removeTransmissionProtocolTx(nftId) 79 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 80 | return events.findEventOrThrow(ProtocolRemovedEvent) 81 | } 82 | 83 | /** 84 | * @name resetTransmissionProtocolTimerTx 85 | * @summary Creates an unsigned unsubmitted Reset-Timer Transaction Hash for an AtBlockWithReset protocol. 86 | * @param nftId The NFT Id to reset the timer for an AtBlockWithReset protocol. 87 | * @param blockNumber The new blockNumber to execute the AtBlockWithReset protocol. 88 | * @returns Unsigned unsubmitted Reset-Timer Transaction Hash. The Hash is only valid for 5 minutes. 89 | */ 90 | export const resetTransmissionProtocolTimerTx = async ( 91 | nftId: number, 92 | blockNumber: number, 93 | ): Promise => { 94 | return await createTxHex(txPallets.transmissionProtocols, txActions.resetTimer, [nftId, blockNumber]) 95 | } 96 | 97 | /** 98 | * @name resetTransmissionProtocolTimer 99 | * @summary Resets the block execution of the transmission protocol. 100 | * @param nftId The NFT Id to remove the transmission protocol. 101 | * @param blockNumber The new blockNumber to execute the AtBlockWithReset protocol. 102 | * @param keyring Account that will sign the transaction. 103 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 104 | * @returns TimerResetEvent Blockchain event. 105 | */ 106 | export const resetTransmissionProtocolTimer = async ( 107 | nftId: number, 108 | blockNumber: number, 109 | keyring: IKeyringPair, 110 | waitUntil: WaitUntil, 111 | ): Promise => { 112 | const tx = await resetTransmissionProtocolTimerTx(nftId, blockNumber) 113 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 114 | return events.findEventOrThrow(TimerResetEvent) 115 | } 116 | 117 | /** 118 | * @name addConsentToOnConsentProtocolTx 119 | * @summary Creates an unsigned unsubmitted Add-Consent Transaction Hash for an OnConsent protocol. 120 | * @param nftId The NFT Id expecting consent to be added by user. 121 | * @returns Unsigned unsubmitted Add-Consent Transaction Hash. The Hash is only valid for 5 minutes. 122 | */ 123 | export const addConsentToOnConsentProtocolTx = async (nftId: number): Promise => { 124 | return await createTxHex(txPallets.transmissionProtocols, txActions.addConsent, [nftId]) 125 | } 126 | 127 | /** 128 | * @name addConsentToOnConsentProtocol 129 | * @summary Adds user consent to transmit the NFT (for users specified in the account list for OnConsent protocol only) 130 | * @param nftId The NFT Id expecting consent to be added by user. 131 | * @param keyring Account that will sign the transaction. 132 | * @param waitUntil Execution trigger that can be set either to BlockInclusion or BlockFinalization. 133 | * @returns ConsentAddedEvent Blockchain event. 134 | */ 135 | export const addConsentToOnConsentProtocol = async ( 136 | nftId: number, 137 | keyring: IKeyringPair, 138 | waitUntil: WaitUntil, 139 | ): Promise => { 140 | const tx = await addConsentToOnConsentProtocolTx(nftId) 141 | const { events } = await submitTxBlocking(tx, waitUntil, keyring) 142 | return events.findEventOrThrow(ConsentAddedEvent) 143 | } 144 | -------------------------------------------------------------------------------- /src/rent/storage.ts: -------------------------------------------------------------------------------- 1 | import { bnToBn } from "@polkadot/util" 2 | 3 | import { 4 | RentalContractChainRawDataType, 5 | RentalContractDataType, 6 | RentingQueuesRawType, 7 | RentingQueuesType, 8 | } from "./types" 9 | 10 | import { blockNumberToDate, query } from "../blockchain" 11 | import { chainQuery, Errors, txPallets } from "../constants" 12 | import { AcceptanceAction, CancellationFeeAction, RentFeeAction } from "./enum" 13 | import { roundBalance } from "../helpers/utils" 14 | 15 | /** 16 | * @name getRentalContractData 17 | * @summary Provides the data related to a rent contract. 18 | * @param nftId The ID of the contracted NFT. 19 | * @returns A JSON object with the rental contract data. 20 | */ 21 | export const getRentalContractData = async (nftId: number): Promise => { 22 | const data = await query(txPallets.rent, chainQuery.contracts, [nftId]) 23 | if (data.isEmpty) { 24 | return null 25 | } 26 | try { 27 | const { 28 | creationBlock, 29 | startBlock, 30 | renter, 31 | rentee, 32 | duration, 33 | acceptanceType, 34 | renterCanRevoke, 35 | rentFee, 36 | renterCancellationFee, 37 | renteeCancellationFee, 38 | } = data.toJSON() as RentalContractChainRawDataType 39 | 40 | const creationBlockDate = await blockNumberToDate(creationBlock) 41 | const startBlockDate = startBlock !== null ? await blockNumberToDate(startBlock) : null 42 | const isManualAcceptance = AcceptanceAction.ManualAcceptance in acceptanceType 43 | const acceptance = isManualAcceptance ? AcceptanceAction.ManualAcceptance : AcceptanceAction.AutoAcceptance 44 | const acceptanceList = acceptanceType.manualAcceptance ?? acceptanceType.autoAcceptance ?? [] 45 | const isRentFeeToken = RentFeeAction.Tokens in rentFee 46 | const rentFeeType = isRentFeeToken ? RentFeeAction.Tokens : RentFeeAction.NFT 47 | const rentFeeValue = isRentFeeToken ? bnToBn(rentFee.tokens).toString() : Number(rentFee.nft) 48 | const rentFeeValueRounded = typeof rentFeeValue === "number" ? rentFeeValue : roundBalance(rentFeeValue) 49 | 50 | let renterCancellationFeeType, renterCancellationFeeValue, renterCancellationFeeValueRounded 51 | switch (true) { 52 | case CancellationFeeAction.FixedTokens in renterCancellationFee: 53 | renterCancellationFeeType = CancellationFeeAction.FixedTokens 54 | renterCancellationFeeValue = bnToBn(renterCancellationFee[renterCancellationFeeType]).toString() 55 | renterCancellationFeeValueRounded = roundBalance(renterCancellationFeeValue) 56 | break 57 | case CancellationFeeAction.FlexibleTokens in renterCancellationFee: 58 | renterCancellationFeeType = CancellationFeeAction.FlexibleTokens 59 | renterCancellationFeeValue = bnToBn(renterCancellationFee[renterCancellationFeeType]).toString() 60 | renterCancellationFeeValueRounded = roundBalance(renterCancellationFeeValue) 61 | break 62 | case CancellationFeeAction.NFT in renterCancellationFee: 63 | renterCancellationFeeType = CancellationFeeAction.NFT 64 | renterCancellationFeeValue = Number(renterCancellationFee[renterCancellationFeeType]) 65 | renterCancellationFeeValueRounded = renterCancellationFeeValue 66 | break 67 | default: 68 | renterCancellationFeeType = CancellationFeeAction.None 69 | renterCancellationFeeValue = null 70 | renterCancellationFeeValueRounded = null 71 | break 72 | } 73 | 74 | let renteeCancellationFeeType, renteeCancellationFeeValue, renteeCancellationFeeValueRounded 75 | switch (true) { 76 | case CancellationFeeAction.FixedTokens in renteeCancellationFee: 77 | renteeCancellationFeeType = CancellationFeeAction.FixedTokens 78 | renteeCancellationFeeValue = bnToBn(renteeCancellationFee[renteeCancellationFeeType]).toString() 79 | renteeCancellationFeeValueRounded = roundBalance(renteeCancellationFeeValue) 80 | break 81 | case CancellationFeeAction.FlexibleTokens in renteeCancellationFee: 82 | renteeCancellationFeeType = CancellationFeeAction.FlexibleTokens 83 | renteeCancellationFeeValue = bnToBn(renteeCancellationFee[renteeCancellationFeeType]).toString() 84 | renteeCancellationFeeValueRounded = roundBalance(renteeCancellationFeeValue) 85 | break 86 | case CancellationFeeAction.NFT in renteeCancellationFee: 87 | renteeCancellationFeeType = CancellationFeeAction.NFT 88 | renteeCancellationFeeValue = Number(renteeCancellationFee[renteeCancellationFeeType]) 89 | renteeCancellationFeeValueRounded = renteeCancellationFeeValue 90 | break 91 | default: 92 | renteeCancellationFeeType = CancellationFeeAction.None 93 | renteeCancellationFeeValue = null 94 | renteeCancellationFeeValueRounded = null 95 | break 96 | } 97 | 98 | return { 99 | creationBlock, 100 | creationBlockDate, 101 | startBlock, 102 | startBlockDate, 103 | renter, 104 | rentee, 105 | duration, 106 | acceptanceType: acceptance, 107 | acceptanceList, 108 | renterCanRevoke, 109 | rentFeeType, 110 | rentFee: rentFeeValue, 111 | rentFeeRounded: rentFeeValueRounded, 112 | renterCancellationFeeType, 113 | renterCancellationFee: renterCancellationFeeValue, 114 | renterCancellationFeeRounded: renterCancellationFeeValueRounded, 115 | renteeCancellationFeeType, 116 | renteeCancellationFee: renteeCancellationFeeValue, 117 | renteeCancellationFeeRounded: renteeCancellationFeeValueRounded, 118 | } as RentalContractDataType 119 | } catch (error) { 120 | throw new Error(`${Errors.RENT_NFT_CONVERSION_ERROR}`) 121 | } 122 | } 123 | 124 | /** 125 | * @name getRentalOffers 126 | * @summary Provides the data related to rent contracts offers. 127 | * @param nftId The ID of the contracted NFT. 128 | * @returns An Array of adresse(s) (string) or null if no offer are available. 129 | */ 130 | export const getRentalOffers = async (nftId: number): Promise => { 131 | const data = await query(txPallets.rent, chainQuery.offers, [nftId]) 132 | const result = data.toJSON() as string[] 133 | return result 134 | } 135 | 136 | /** 137 | * @name getRentingQueues 138 | * @summary Provides the deadlines related to contracts in queues for available contracts, running fixed contract and running subscribed contract. 139 | * @returns An object containing an array with NFT ID, the block expriation ID for each fixedQueue, subscriptionQueue or availableQueue. See the RentingQueuesType type. 140 | */ 141 | export const getRentingQueues = async (): Promise => { 142 | const data = await query(txPallets.rent, chainQuery.queues) 143 | try { 144 | const { fixedQueue, subscriptionQueue, availableQueue } = data.toJSON() as RentingQueuesRawType 145 | 146 | return { 147 | fixedQueue: fixedQueue.map((queue) => ({ 148 | nftId: queue[0], 149 | endingBlockId: queue[1], 150 | })), 151 | subscriptionQueue: subscriptionQueue.map((queue) => ({ 152 | nftId: queue[0], 153 | renewalOrEndBlockId: queue[1], 154 | })), 155 | availableQueue: availableQueue.map((queue) => ({ 156 | nftId: queue[0], 157 | expirationBlockId: queue[1], 158 | })), 159 | } as RentingQueuesType 160 | } catch (error) { 161 | throw new Error(`${Errors.RENT_NFT_CONVERSION_ERROR}`) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum txPallets { 2 | assets = "assets", 3 | auction = "auction", 4 | marketplace = "marketplace", 5 | nft = "nft", 6 | rent = "rent", 7 | utility = "utility", 8 | balances = "balances", 9 | associatedAccounts = "associatedAccounts", 10 | system = "system", 11 | tee = "tee", 12 | transmissionProtocols = "transmissionProtocols", 13 | } 14 | 15 | export enum txActions { 16 | create = "create", 17 | transfer = "transfer", 18 | transferAll = "transferAll", 19 | transferKeepAlive = "transferKeepAlive", 20 | batch = "batch", 21 | batchAll = "batchAll", 22 | forceBatch = "forceBatch", 23 | 24 | // nft 25 | addSecret = "addSecret", 26 | createNft = "createNft", 27 | createSecretNft = "createSecretNft", 28 | burnNft = "burnNft", 29 | transferNft = "transferNft", 30 | delegateNft = "delegateNft", 31 | setRoyalty = "setRoyalty", 32 | addNftToCollection = "addNftToCollection", 33 | createCollection = "createCollection", 34 | limitCollection = "limitCollection", 35 | closeCollection = "closeCollection", 36 | burnCollection = "burnCollection", 37 | setCollectionOffchaindata = "setCollectionOffchaindata", 38 | setNftMintFee = "setNftMintFee", 39 | 40 | // capsule 41 | convertToCapsule = "convertToCapsule", 42 | createCapsule = "createCapsule", 43 | revertCapsule = "revertCapsule", 44 | setCapsuleOffchaindata = "setCapsuleOffchaindata", 45 | notifyEnclaveKeyUpdate = "notifyEnclaveKeyUpdate", 46 | 47 | // rent 48 | createContract = "createContract", 49 | cancelContract = "cancelContract", 50 | acceptRentOffer = "acceptRentOffer", 51 | acceptSubscriptionTerms = "acceptSubscriptionTerms", 52 | changeSubscriptionTerms = "changeSubscriptionTerms", 53 | rent = "rent", 54 | makeRentOffer = "makeRentOffer", 55 | retractRentOffer = "retractRentOffer", 56 | revokeContract = "revokeContract", 57 | 58 | // transmission protocols 59 | addConsent = "addConsent", 60 | removeTransmissionProtocol = "removeTransmissionProtocol", 61 | resetTimer = "resetTimer", 62 | setTransmissionProtocol = "setTransmissionProtocol", 63 | 64 | // marketplace 65 | buyNft = "buyNft", 66 | createMarketplace = "createMarketplace", 67 | listNft = "listNft", 68 | unlistNft = "unlistNft", 69 | setMarketplaceConfiguration = "setMarketplaceConfiguration", 70 | setMarketplaceKind = "setMarketplaceKind", 71 | setMarketplaceOwner = "setMarketplaceOwner", 72 | setMarketplaceMintFee = "setMarketplaceMintFee", 73 | 74 | // auction 75 | createAuction = "createAuction", 76 | cancelAuction = "cancelAuction", 77 | endAuction = "endAuction", 78 | addBid = "addBid", 79 | removeBid = "removeBid", 80 | buyItNow = "buyItNow", 81 | claim = "claim", 82 | 83 | // tee 84 | submitMetricsServerReport = "submitMetricsServerReport", 85 | claimRewards = "claimRewards", 86 | } 87 | 88 | export enum txEvent { 89 | ExtrinsicSuccess = "ExtrinsicSuccess", 90 | ExtrinsicFailed = "ExtrinsicFailed", 91 | BatchCompleted = "BatchCompleted", 92 | BatchInterrupted = "BatchInterrupted", 93 | nftsCreated = "Created", 94 | nftsBurned = "Burned", 95 | nftsTransfered = "Transfered", 96 | MarketplaceCreated = "MarketplaceCreated", 97 | } 98 | 99 | export enum chainQuery { 100 | nftMintFee = "nftMintFee", 101 | secretNftMintFee = "secretNftMintFee", 102 | secretNftsOffchainData = "secretNftsOffchainData", 103 | nfts = "nfts", 104 | nextNFTId = "nextNFTId", 105 | nextCollectionId = "nextCollectionId", 106 | marketplaceMintFee = "marketplaceMintFee", 107 | account = "account", 108 | number = "number", 109 | collections = "collections", 110 | nextMarketplaceId = "nextMarketplaceId", 111 | marketplaces = "marketplaces", 112 | listedNfts = "listedNfts", 113 | 114 | // capsule 115 | capsuleMintFee = "capsuleMintFee", 116 | capsuleOffchainData = "capsuleOffchainData", 117 | 118 | // transmissionProtocols 119 | atBlockFee = "atBlockFee", 120 | atBlockWithResetFee = "atBlockWithResetFee", 121 | onConsentFee = "onConsentFee", 122 | onConsentAtBlockFee = "onConsentAtBlockFee", 123 | atBlockQueue = "atBlockQueue", 124 | transmissions = "transmissions", 125 | onConsentData = "onConsentData", 126 | 127 | // auction 128 | auctions = "auctions", 129 | deadlines = "deadlines", 130 | claims = "claims", 131 | 132 | // rent 133 | contracts = "contracts", 134 | queues = "queues", 135 | offers = "offers", 136 | 137 | // tee 138 | clusterData = "clusterData", 139 | enclaveData = "enclaveData", 140 | nextClusterId = "nextClusterId", 141 | } 142 | 143 | export enum chainConstants { 144 | initialMintFee = "initialMintFee", 145 | initialSecretMintFee = "initialSecretMintFee", 146 | collectionSizeLimit = "collectionSizeLimit", 147 | existentialDeposit = "existentialDeposit", 148 | nftOffchainDataLimit = "nftOffchainDataLimit", 149 | collectionOffchainDataLimit = "collectionOffchainDataLimit", 150 | offchainDataLimit = "offchainDataLimit", 151 | accountSizeLimit = "accountSizeLimit", 152 | 153 | // auction 154 | auctionEndingPeriod = "auctionEndingPeriod", 155 | auctionGracePeriod = "auctionGracePeriod", 156 | bidderListLengthLimit = "bidderListLengthLimit", 157 | maxAuctionDelay = "maxAuctionDelay", 158 | minAuctionDuration = "minAuctionDuration", 159 | maxAuctionDuration = "maxAuctionDuration", 160 | parallelAuctionLimit = "parallelAuctionLimit", 161 | 162 | // rent 163 | actionsInBlockLimit = "actionsInBlockLimit", 164 | maximumContractAvailabilityLimit = "maximumContractAvailabilityLimit", 165 | maximumContractDurationLimit = "maximumContractDurationLimit", 166 | simultaneousContractLimit = "simultaneousContractLimit", 167 | 168 | // transmissionProtocols 169 | simultaneousTransmissionLimit = "simultaneousTransmissionLimit", 170 | maxConsentListSize = "maxConsentListSize", 171 | maxBlockDuration = "maxBlockDuration", 172 | } 173 | 174 | export enum WaitUntil { 175 | BlockInclusion, 176 | BlockFinalization, 177 | } 178 | 179 | export enum Errors { 180 | EXTRINSIC_FAILED = "EXTRINSIC_FAILED", 181 | EVENT_NOT_FOUND = "EVENT_NOT_FOUND", 182 | SEED_NOT_FOUND = "SEED_NOT_FOUND", 183 | PUBLIC_SEED_ADDRESS_NOT_FOUND = "PUBLIC_SEED_ADDRESS_NOT_FOUND", 184 | VALUE_MUST_BE_GREATER_THAN_0 = "VALUE_MUST_BE_GREATER_THAN_0", 185 | VALUE_MUST_BE_DEFINED = "VALUE_MUST_BE_DEFINED", 186 | INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS", 187 | API_NOT_INITIALIZED = "API_NOT_INITIALIZED", 188 | API_NOT_CONNECTED = "API_NOT_CONNECTED", 189 | TRANSACTION_NOT_IN_BLOCK = "TRANSACTION_NOT_IN_BLOCK", 190 | EXTRINSIC_NOT_FOUND = "EXTRINSIC_NOT_FOUND", 191 | OFFCHAIN_LENGTH_TOO_HIGH = "OFFCHAIN_LENGTH_TOO_HIGH", 192 | LIMIT_TOO_LOW = "LIMIT_TOO_LOW", 193 | LIMIT_TOO_HIGH = "LIMIT_TOO_HIGH", 194 | NFT_NOT_FOUND = "NFT_NOT_FOUND", 195 | COLLECTION_NOT_FOUND = "COLLECTION_NOT_FOUND", 196 | MUST_BE_A_NUMBER = "MUST_BE_A_NUMBER", 197 | URL_UNDEFINED = "URL_UNDEFINED", 198 | MUST_BE_PERCENTAGE = "MUST_BE_PERCENTAGE", 199 | NFT_CONVERSION_ERROR = "NFT_CONVERSION_ERROR", 200 | COLLECTION_CONVERSION_ERROR = "COLLECTION_CONVERSION_ERROR", 201 | MARKETPLACE_CONVERSION_ERROR = "MARKETPLACE_CONVERSION_ERROR", 202 | LISTED_NFT_CONVERSION_ERROR = "LISTED_NFT_CONVERSION_ERROR", 203 | IPFS_FILE_UPLOAD_ERROR = "IPFS_FILE_UPLOAD_ERROR", 204 | IPFS_METADATA_VALIDATION_ERROR = "IPFS_METADATA_VALIDATION_ERROR", 205 | BLOCK_NOT_FOUND_ON_CHAIN = "BLOCK_NOT_FOUND_ON_CHAIN", 206 | AUCTION_NFT_CONVERSION_ERROR = "AUCTION_NFT_CONVERSION_ERROR", 207 | RENT_NFT_CONVERSION_ERROR = "RENT_NFT_CONVERSION_ERROR", 208 | CLUSTER_CONVERSION_ERROR = "CLUSTER_CONVERSION_ERROR", 209 | ENCLAVE_CONVERSION_ERROR = "ENCLAVE_CONVERSION_ERROR", 210 | TRANSMISSION_PROTOCOL_CONVERSION_ERROR = "TRANSMISSION_PROTOCOL_CONVERSION_ERROR", 211 | TEE_CLUSTER_NOT_FOUND = "TEE_CLUSTER_NOT_FOUND", 212 | NEXT_TEE_CLUSTER_UNDEFINED = "NEXT_TEE_CLUSTER_UNDEFINED", 213 | TEE_CLUSTER_IS_EMPTY = "TEE_CLUSTER_IS_EMPTY", 214 | TEE_ENCLAVE_NOT_FOUND = "TEE_ENCLAVE_NOT_FOUND", 215 | TEE_ENCLAVE_NOT_AVAILBLE = "TEE_ENCLAVE_NOT_AVAILBLE", 216 | TEE_UPLOAD_ERROR = "TEE_UPLOAD_ERROR", 217 | TEE_RETRIEVE_ERROR = "TEE_RETRIEVE_ERROR", 218 | TEE_REMOVE_ERROR = "TEE_REMOVE_ERROR", 219 | TEE_ERROR = "TEE_ERROR", 220 | NOT_CORRECT_AMOUNT_TEE_PAYLOADS = "NOT_CORRECT_AMOUNT_TEE_PAYLOADS", 221 | NOT_CORRECT_AMOUNT_TEE_ENCLAVES = "NOT_CORRECT_AMOUNT_TEE_ENCLAVES", 222 | NFT_RECONCILIATION_FAILED = "NFT_RECONCILIATION_FAILED", 223 | RECONCILIATION_PAYLOAD_UNDEFINED = "RECONCILIATION_PAYLOAD_UNDEFINED", 224 | } 225 | -------------------------------------------------------------------------------- /src/rent/extrinsics.test.ts: -------------------------------------------------------------------------------- 1 | import { AcceptanceAction, CancellationFeeAction, DurationAction, RentFeeAction } from "./enum" 2 | import { 3 | acceptRentOffer, 4 | acceptSubscriptionTerms, 5 | cancelContract, 6 | changeSubscriptionTerms, 7 | createContract, 8 | makeRentOffer, 9 | rent, 10 | retractRentOffer, 11 | revokeContract, 12 | } from "./extrinsics" 13 | import { formatAcceptanceType, formatCancellationFee, formatDuration, formatRentFee } from "./utils" 14 | 15 | import { initializeApi, numberToBalance } from "../blockchain" 16 | import { createNft } from "../nft" 17 | import { WaitUntil } from "../constants" 18 | import { createTestPairs } from "../_misc/testingPairs" 19 | 20 | const TEST_DATA = { 21 | nftId: 0, 22 | contractCreationBlockId: 0, 23 | } 24 | beforeAll(async () => { 25 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 26 | await initializeApi(endpoint) 27 | // Create some Test NFT 28 | const { test: testAccount } = await createTestPairs() 29 | const nEvent = await createNft("Test NFT Data", 0, undefined, false, testAccount, WaitUntil.BlockInclusion) 30 | TEST_DATA.nftId = nEvent.nftId 31 | }) 32 | 33 | describe("Testing Rent extrinsics", (): void => { 34 | it("Testing to create a rent contract", async (): Promise => { 35 | const { dest: destAccount, test: testAccount } = await createTestPairs() 36 | const duration = formatDuration("subscription", 30, 100, true) 37 | const acceptanceType = formatAcceptanceType("manual", [destAccount.address]) 38 | const rentFee = formatRentFee("tokens", 1) 39 | const renterCancellationFee = formatCancellationFee("fixed", 1) 40 | const renteeCancellationFee = formatCancellationFee("none") 41 | const contractEvent = await createContract( 42 | TEST_DATA.nftId, 43 | duration, 44 | acceptanceType, 45 | true, 46 | rentFee, 47 | renterCancellationFee, 48 | renteeCancellationFee, 49 | testAccount, 50 | WaitUntil.BlockInclusion, 51 | ) 52 | TEST_DATA.contractCreationBlockId = contractEvent.creationBlockId 53 | expect( 54 | contractEvent.nftId === TEST_DATA.nftId && 55 | contractEvent.renter === testAccount.address && 56 | contractEvent.creationBlockId > 0 && 57 | DurationAction.Subscription in contractEvent.duration && 58 | contractEvent.duration[DurationAction.Subscription].periodLength === 30 && 59 | contractEvent.duration[DurationAction.Subscription].maxDuration === 100 && 60 | contractEvent.duration[DurationAction.Subscription].isChangeable === true && 61 | contractEvent.duration[DurationAction.Subscription].newTerms === false && 62 | contractEvent.acceptanceType === AcceptanceAction.ManualAcceptance && 63 | contractEvent.acceptanceList?.includes(destAccount.address) && 64 | contractEvent.acceptanceList?.length === 1 && 65 | contractEvent.renterCanRevoke === true && 66 | contractEvent.rentFeeType === RentFeeAction.Tokens && 67 | contractEvent.rentFee === numberToBalance(1).toString() && 68 | contractEvent.rentFeeRounded === 1 && 69 | contractEvent.renterCancellationFeeType === CancellationFeeAction.FixedTokens && 70 | contractEvent.renterCancellationFee === numberToBalance(1).toString() && 71 | contractEvent.renterCancellationFeeRounded === 1 && 72 | contractEvent.renteeCancellationFeeType === CancellationFeeAction.None && 73 | contractEvent.renteeCancellationFee === null && 74 | contractEvent.renteeCancellationFeeRounded === null, 75 | ).toBe(true) 76 | }) 77 | 78 | it("Should return the address who made the offer on an NFT contract", async () => { 79 | const { dest: destAccount } = await createTestPairs() 80 | const { rentee, nftId } = await makeRentOffer( 81 | TEST_DATA.nftId, 82 | TEST_DATA.contractCreationBlockId, 83 | destAccount, 84 | WaitUntil.BlockInclusion, 85 | ) 86 | expect(rentee === destAccount.address && nftId === TEST_DATA.nftId).toBe(true) 87 | }) 88 | 89 | it("Should return the address who retracted the offer", async () => { 90 | const { dest: destAccount } = await createTestPairs() 91 | const { rentee, nftId } = await retractRentOffer(TEST_DATA.nftId, destAccount, WaitUntil.BlockInclusion) 92 | expect(rentee === destAccount.address && nftId === TEST_DATA.nftId).toBe(true) 93 | }) 94 | 95 | it("Should return the rentee address of the accepted offer when a contract started", async () => { 96 | const { dest: destAccount, test: testAccount } = await createTestPairs() 97 | await makeRentOffer(TEST_DATA.nftId, TEST_DATA.contractCreationBlockId, destAccount, WaitUntil.BlockInclusion) 98 | const { rentee, nftId } = await acceptRentOffer( 99 | TEST_DATA.nftId, 100 | destAccount.address, 101 | testAccount, 102 | WaitUntil.BlockInclusion, 103 | ) 104 | expect(rentee === destAccount.address && nftId === TEST_DATA.nftId).toBe(true) 105 | }) 106 | }) 107 | 108 | describe("Testing to update and revoke a subscription contract", (): void => { 109 | it("Should return the updated terms of the contract", async () => { 110 | const { test: testAccount } = await createTestPairs() 111 | const contractEvent = await changeSubscriptionTerms( 112 | TEST_DATA.nftId, 113 | 3, 114 | 10, 115 | 100, 116 | true, 117 | testAccount, 118 | WaitUntil.BlockInclusion, 119 | ) 120 | const rentFee = numberToBalance(3).toString() 121 | expect( 122 | contractEvent.nftId === TEST_DATA.nftId && 123 | contractEvent.period === 10 && 124 | contractEvent.maxDuration === 100 && 125 | contractEvent.isChangeable === true && 126 | contractEvent.rentFeeType === RentFeeAction.Tokens && 127 | contractEvent.rentFee === rentFee && 128 | contractEvent.rentFeeRounded === 3, 129 | ).toBe(true) 130 | }) 131 | it("Should return the nftId of the new updated and accepted contract", async () => { 132 | const { dest: destAccount } = await createTestPairs() 133 | const { nftId } = await acceptSubscriptionTerms( 134 | TEST_DATA.nftId, 135 | 3, 136 | 10, 137 | 100, 138 | true, 139 | destAccount, 140 | WaitUntil.BlockInclusion, 141 | ) 142 | expect(nftId === TEST_DATA.nftId).toBe(true) 143 | }) 144 | 145 | it("Should return the address of the revoker of the contract", async () => { 146 | const { dest: destAccount } = await createTestPairs() 147 | const { nftId, revokedBy } = await revokeContract(TEST_DATA.nftId, destAccount, WaitUntil.BlockInclusion) 148 | expect(revokedBy === destAccount.address && nftId === TEST_DATA.nftId).toBe(true) 149 | }) 150 | }) 151 | 152 | describe("Testing to rent or cancel a contract", (): void => { 153 | it("Should return the nftId of the rented contract", async () => { 154 | const { test: testAccount, dest: destAccount } = await createTestPairs() 155 | const duration = formatDuration("subscription", 5, 10) 156 | const acceptanceType = formatAcceptanceType("auto") 157 | const rentFee = formatRentFee("tokens", 1) 158 | const renterCancellationFee = formatCancellationFee("none") 159 | const renteeCancellationFee = formatCancellationFee("none") 160 | const contractEvent = await createContract( 161 | TEST_DATA.nftId, 162 | duration, 163 | acceptanceType, 164 | true, 165 | rentFee, 166 | renterCancellationFee, 167 | renteeCancellationFee, 168 | testAccount, 169 | WaitUntil.BlockInclusion, 170 | ) 171 | const { nftId } = await rent(TEST_DATA.nftId, contractEvent.creationBlockId, destAccount, WaitUntil.BlockInclusion) 172 | expect(nftId === TEST_DATA.nftId).toBe(true) 173 | }) 174 | it("Should return the nftId of the cancelled contract", async () => { 175 | const { test: testAccount, dest: destAccount } = await createTestPairs() 176 | await revokeContract(TEST_DATA.nftId, destAccount, WaitUntil.BlockInclusion) 177 | const duration = formatDuration("fixed", 1000) 178 | const acceptanceType = formatAcceptanceType("auto") 179 | const rentFee = formatRentFee("tokens", 1) 180 | const renterCancellationFee = formatCancellationFee("none") 181 | const renteeCancellationFee = formatCancellationFee("none") 182 | await createContract( 183 | TEST_DATA.nftId, 184 | duration, 185 | acceptanceType, 186 | true, 187 | rentFee, 188 | renterCancellationFee, 189 | renteeCancellationFee, 190 | testAccount, 191 | WaitUntil.BlockInclusion, 192 | ) 193 | const { nftId } = await cancelContract(TEST_DATA.nftId, testAccount, WaitUntil.BlockInclusion) 194 | expect(nftId === TEST_DATA.nftId).toBe(true) 195 | }) 196 | }) 197 | -------------------------------------------------------------------------------- /src/blockchain/index.test.ts: -------------------------------------------------------------------------------- 1 | import { isHex } from "@polkadot/util" 2 | import { BN, isBN } from "bn.js" 3 | 4 | import { 5 | batchTxHex, 6 | batchAllTxHex, 7 | consts, 8 | createTxHex, 9 | initializeApi, 10 | query, 11 | submitTxHex, 12 | balanceToNumber, 13 | numberToBalance, 14 | isValidSignature, 15 | getTxInitialFee, 16 | submitTxBlocking, 17 | checkFundsForTxFees, 18 | createTx, 19 | getTxAdditionalFee, 20 | getTxFees, 21 | signTxHex, 22 | forceBatchTxHex, 23 | } from "." 24 | import { generateSeed, getKeyringFromSeed } from "../account" 25 | import { chainConstants, chainQuery, Errors, txActions, txPallets, WaitUntil } from "../constants" 26 | import { BalancesTransferEvent, BalancesWithdrawEvent, ExtrinsicSuccessEvent } from "../events" 27 | import { getNftMintFee } from "../nft" 28 | import { createTestPairs } from "../_misc/testingPairs" 29 | 30 | beforeAll(async () => { 31 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 32 | return initializeApi(endpoint) 33 | }) 34 | 35 | it("createTxHex should return a correct transaction hash hex", async () => { 36 | const { test: testAccount } = await createTestPairs() 37 | const txHex = await createTxHex(txPallets.balances, txActions.transfer, [testAccount.address, "10000000000000000"]) 38 | expect(isHex(txHex)).toBe(true) 39 | }) 40 | 41 | it("signedTxHex should return a correct signable transaction hash hex", async () => { 42 | const { test: testAccount } = await createTestPairs() 43 | const txHex = await createTxHex(txPallets.balances, txActions.transfer, [testAccount.address, "10000000000000000"]) 44 | const signedTxHex = await signTxHex(testAccount, txHex) 45 | expect(isHex(signedTxHex)).toBe(true) 46 | }) 47 | 48 | it("submitTxHex should return a correct submited transaction hash hex", async () => { 49 | const { test: testAccount, dest: destAccount } = await createTestPairs() 50 | const txHex = await createTxHex(txPallets.balances, txActions.transfer, [destAccount.address, "1000000000000000"]) 51 | const signedTxHex = await signTxHex(testAccount, txHex) 52 | const submittedTxHex = await submitTxHex(signedTxHex) 53 | expect(isHex(submittedTxHex)).toBe(true) 54 | }) 55 | 56 | it("batchTxHex should return a correct batch transaction hash hex", async () => { 57 | const { dest: destAccount } = await createTestPairs() 58 | const txHex1 = await createTxHex(txPallets.balances, txActions.transfer, [destAccount.address, "1000000000000000"]) 59 | const txHex2 = await createTxHex(txPallets.balances, txActions.transfer, [destAccount.address, "2000000000000000"]) 60 | const batchTx = await batchTxHex([txHex1, txHex2]) 61 | expect(isHex(batchTx)).toBe(true) 62 | }) 63 | 64 | it("batchAllTxHex should return a correct batchAll transaction hash hex", async () => { 65 | const { dest: destAccount } = await createTestPairs() 66 | const txHex1 = await createTxHex(txPallets.balances, txActions.transfer, [destAccount.address, "1000000000000000"]) 67 | const txHex2 = await createTxHex(txPallets.balances, txActions.transfer, [destAccount.address, "2000000000000000"]) 68 | const batchAllTx = await batchAllTxHex([txHex1, txHex2]) 69 | expect(isHex(batchAllTx)).toBe(true) 70 | }) 71 | 72 | it("forceBatchTxHex should return a correct batch transaction hash hex", async () => { 73 | const { dest: destAccount } = await createTestPairs() 74 | const txHex1 = await createTxHex(txPallets.balances, txActions.transfer, [destAccount.address, "1000000000000000"]) 75 | const txHex2 = await createTxHex(txPallets.balances, txActions.transfer, [destAccount.address, "2000000000000000"]) 76 | const forceBatchTx = await forceBatchTxHex([txHex1, txHex2]) 77 | expect(isHex(forceBatchTx)).toBe(true) 78 | }) 79 | 80 | it("submitTxBlocking should contain BalancesTransfer and ExtrinsicSuccess events on a succesful balance transfer transaction", async () => { 81 | const { test: testAccount, dest: destAccount } = await createTestPairs() 82 | const txHex = await createTxHex(txPallets.balances, txActions.transfer, [destAccount.address, "1000000000000000"]) 83 | const { events } = await submitTxBlocking(txHex, WaitUntil.BlockInclusion, testAccount) 84 | const isSuccess = 85 | Boolean(events.findEvent(BalancesWithdrawEvent)) && 86 | Boolean(events.findEvent(BalancesTransferEvent)) && 87 | Boolean(events.findEvent(ExtrinsicSuccessEvent)) 88 | expect(isSuccess).toBe(true) 89 | }) 90 | 91 | describe("Constants", (): void => { 92 | it("Should get the correct existensial deposit", async (): Promise => { 93 | const existensialDeposit = consts(txPallets.balances, chainConstants.existentialDeposit) 94 | expect(existensialDeposit).toBeDefined() 95 | }) 96 | it("Should throw error with inexisting consts", async () => { 97 | await expect(async () => { 98 | consts("toBe", "orNotToBe") 99 | }).rejects.toThrow(TypeError) 100 | }) 101 | }) 102 | 103 | describe("Storage query", (): void => { 104 | it("Should be able to query storage data", async () => { 105 | const data = await query(txPallets.system, chainQuery.number) 106 | expect(data).toBeDefined() 107 | }) 108 | it("Should throw error with inexisting storage", async () => { 109 | await expect(async () => { 110 | await query("toBe", "orNotToBe") 111 | }).rejects.toThrow(TypeError) 112 | }) 113 | }) 114 | 115 | describe("Fee getters", (): void => { 116 | it("Should get initial fee estimation for a transaction", async () => { 117 | const { test: testAccount } = await createTestPairs() 118 | const txHex = await createTxHex(txPallets.nft, txActions.createNft, ["offchain data", 0, undefined, false]) 119 | const txInitialFee = await getTxInitialFee(txHex, testAccount.address) 120 | expect(isBN(txInitialFee)).toBe(true) 121 | }) 122 | 123 | it("Should get additional fee for NFT creation", async () => { 124 | const nftMintFee = await getNftMintFee() 125 | const txHex = await createTxHex(txPallets.nft, txActions.createNft, ["offchain data", 0, undefined, false]) 126 | const txAdditionalFee = await getTxAdditionalFee(txHex) 127 | expect(txAdditionalFee.eq(nftMintFee)).toBe(true) 128 | }) 129 | 130 | it("Should get zero additional fee for a default transaction (e.g. a balance transfer)", async () => { 131 | const { test: testAccount } = await createTestPairs() 132 | const txHex = await createTxHex(txPallets.balances, txActions.transfer, [testAccount.address, "10000000000000000"]) 133 | const txAdditionalFee = await getTxAdditionalFee(txHex) 134 | expect(txAdditionalFee.isZero()).toBe(true) 135 | }) 136 | 137 | it("Should get total fee estimation for a transaction", async () => { 138 | const { test: testAccount } = await createTestPairs() 139 | const nftMintFee = await getNftMintFee() 140 | const txHex = await createTxHex(txPallets.nft, txActions.createNft, ["offchain data", 0, undefined, false]) 141 | const txFee = await getTxFees(txHex, testAccount.address) 142 | expect(txFee.gt(nftMintFee)).toBe(true) 143 | }) 144 | 145 | xit("Should throw an error if insufficient funds for fees", async () => { 146 | const seed = generateSeed() 147 | const keyring = await getKeyringFromSeed(seed) 148 | const txHex = await createTx(txPallets.balances, txActions.transfer, [keyring.address, "10000000000000000000"]) 149 | await expect(async () => { 150 | await checkFundsForTxFees(txHex) 151 | }).rejects.toThrow(Error(Errors.INSUFFICIENT_FUNDS)) 152 | }) 153 | }) 154 | 155 | describe("Balance formatting", (): void => { 156 | it("Should format a BN into a number", async () => { 157 | const res = balanceToNumber(new BN("123432100000000000000000000")) 158 | expect(res).toBe("123.4321 MCAPS") 159 | }) 160 | it("Should unformat a number into a BN", async () => { 161 | const res = numberToBalance(123.4321) 162 | expect(res).toEqual(new BN("123432100000000000000")) 163 | }) 164 | }) 165 | 166 | describe("isValidSignature", (): void => { 167 | it("Should return true if a message passed as parameter has been signed by the passed address", async () => { 168 | expect( 169 | isValidSignature( 170 | "This is a text message", 171 | "0x2aeaa98e26062cf65161c68c5cb7aa31ca050cb5bdd07abc80a475d2a2eebc7b7a9c9546fbdff971b29419ddd9982bf4148c81a49df550154e1674a6b58bac84", 172 | "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", 173 | ), 174 | ).toBe(true) 175 | }) 176 | it("Should return false if a message passed as parameter has not been signed by the passed address", async () => { 177 | const { test: testAccount } = await createTestPairs() 178 | expect( 179 | isValidSignature( 180 | "This is a text message", 181 | "0x2aeaa98e26062cf65161c68c5cb7aa31ca050cb5bdd07abc80a475d2a2eebc7b7a9c9546fbdff971b29419ddd9982bf4148c81a49df550154e1674a6b58bac84", 182 | testAccount.address, 183 | ), 184 | ).toBe(false) 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /src/marketplace/extrinsics.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestPairs } from "../_misc/testingPairs" 2 | import { initializeApi, numberToBalance } from "../blockchain" 3 | import { WaitUntil } from "../constants" 4 | import { createCollection, createNft, getNftData } from "../nft" 5 | 6 | import { MarketplaceConfigAction, MarketplaceConfigFeeType, MarketplaceKind } from "./enum" 7 | import { 8 | buyNft, 9 | createMarketplace, 10 | listNft, 11 | setMarketplaceConfiguration, 12 | setMarketplaceKind, 13 | setMarketplaceOwner, 14 | unlistNft, 15 | } from "./extrinsics" 16 | import { 17 | formatMarketplaceAccountList, 18 | formatMarketplaceCollectionList, 19 | formatMarketplaceFee, 20 | formatMarketplaceOffchainData, 21 | } from "./utils" 22 | 23 | const TEST_DATA = { 24 | nftId: 0, 25 | marketplaceId: 0, 26 | } 27 | 28 | beforeAll(async () => { 29 | const endpoint: string | undefined = process.env.BLOCKCHAIN_ENDPOINT 30 | await initializeApi(endpoint) 31 | }) 32 | 33 | describe("Testing Marketplace extrinsics", (): void => { 34 | it("Testing to create a marketplace", async (): Promise => { 35 | const { test: testAccount } = await createTestPairs() 36 | const mpEvent = await createMarketplace(MarketplaceKind.Public, testAccount, WaitUntil.BlockInclusion) 37 | TEST_DATA.marketplaceId = mpEvent.marketplaceId 38 | expect( 39 | mpEvent.marketplaceId >= 0 && mpEvent.kind === MarketplaceKind.Public && mpEvent.owner === testAccount.address, 40 | ).toBe(true) 41 | }) 42 | 43 | it("Testing to set all marketplace parameters configuration", async (): Promise => { 44 | const { test: testAccount, dest: destAccount } = await createTestPairs() 45 | const { collectionId } = await createCollection( 46 | "SDK_COLLECTION_TESTING", 47 | undefined, 48 | destAccount, 49 | WaitUntil.BlockInclusion, 50 | ) 51 | const formattedCommissionFee = formatMarketplaceFee( 52 | MarketplaceConfigAction.Set, 53 | MarketplaceConfigFeeType.Percentage, 54 | 10, 55 | ) 56 | const formattedListingFee = formatMarketplaceFee(MarketplaceConfigAction.Set, MarketplaceConfigFeeType.Flat, 100) 57 | const formattedAccountList = formatMarketplaceAccountList(MarketplaceConfigAction.Set, [destAccount.address]) 58 | const formattedOffchainData = formatMarketplaceOffchainData( 59 | MarketplaceConfigAction.Set, 60 | "SDK_MARKETPLACE_CONFIG_TEST", 61 | ) 62 | const formattedCollectionList = formatMarketplaceCollectionList(MarketplaceConfigAction.Set, [collectionId]) 63 | const mpEvent = await setMarketplaceConfiguration( 64 | TEST_DATA.marketplaceId, 65 | formattedCommissionFee, 66 | formattedListingFee, 67 | formattedAccountList, 68 | formattedOffchainData, 69 | formattedCollectionList, 70 | testAccount, 71 | WaitUntil.BlockInclusion, 72 | ) 73 | const listingFee = numberToBalance(100).toString() 74 | expect( 75 | mpEvent.marketplaceId === TEST_DATA.marketplaceId && 76 | mpEvent.commissionFee === "10" && 77 | mpEvent.commissionFeeRounded === 10 && 78 | mpEvent.commissionFeeType === MarketplaceConfigFeeType.Percentage && 79 | mpEvent.listingFee === listingFee && 80 | mpEvent.listingFeeRounded === 100 && 81 | mpEvent.listingFeeType === MarketplaceConfigFeeType.Flat && 82 | mpEvent.accountList?.length === 1 && 83 | mpEvent.accountList?.includes(destAccount.address) && 84 | mpEvent.offchainData === "SDK_MARKETPLACE_CONFIG_TEST" && 85 | mpEvent.collectionList?.includes(collectionId), 86 | ).toBe(true) 87 | }) 88 | it("Testing to Remove and keep(Noop) the marketplace parameters configuration", async (): Promise => { 89 | const { test: testAccount } = await createTestPairs() 90 | const formattedCommissionFee = formatMarketplaceFee(MarketplaceConfigAction.Noop) 91 | const formattedListingFee = formatMarketplaceFee(MarketplaceConfigAction.Remove) 92 | const formattedAccountList = formatMarketplaceAccountList(MarketplaceConfigAction.Remove) 93 | const formattedOffchainData = formatMarketplaceOffchainData(MarketplaceConfigAction.Noop) 94 | const formattedCollectionList = formatMarketplaceCollectionList(MarketplaceConfigAction.Remove) 95 | const mpEvent = await setMarketplaceConfiguration( 96 | TEST_DATA.marketplaceId, 97 | formattedCommissionFee, 98 | formattedListingFee, 99 | formattedAccountList, 100 | formattedOffchainData, 101 | formattedCollectionList, 102 | testAccount, 103 | WaitUntil.BlockInclusion, 104 | ) 105 | expect( 106 | mpEvent.commissionFee === undefined && 107 | mpEvent.commissionFeeRounded === undefined && 108 | mpEvent.commissionFeeType === undefined && 109 | mpEvent.listingFee === null && 110 | mpEvent.listingFeeRounded === null && 111 | mpEvent.listingFeeType === null && 112 | mpEvent.accountList?.length === 0 && 113 | mpEvent.offchainData === undefined && 114 | mpEvent.collectionList?.length === 0, 115 | ).toBe(true) 116 | }) 117 | 118 | it("Testing to set new marketplace owner", async (): Promise => { 119 | const { test: testAccount, dest: destAccount } = await createTestPairs() 120 | const mpEvent = await setMarketplaceOwner( 121 | TEST_DATA.marketplaceId, 122 | destAccount.address, 123 | testAccount, 124 | WaitUntil.BlockInclusion, 125 | ) 126 | expect(mpEvent.marketplaceId === TEST_DATA.marketplaceId && mpEvent.owner === destAccount.address).toBe(true) 127 | }) 128 | 129 | it("Testing to set marketplace kind", async (): Promise => { 130 | const { dest: destAccount } = await createTestPairs() 131 | const mpEvent = await setMarketplaceKind( 132 | TEST_DATA.marketplaceId, 133 | MarketplaceKind.Private, 134 | destAccount, 135 | WaitUntil.BlockInclusion, 136 | ) 137 | expect(mpEvent.marketplaceId === TEST_DATA.marketplaceId && mpEvent.kind === MarketplaceKind.Private).toBe(true) 138 | }) 139 | }) 140 | 141 | describe("Testing to List, Unlist, Buy an NFT on the Marketplace", (): void => { 142 | it("Testing to List an NFT on a marketplace", async (): Promise => { 143 | const { dest: destAccount } = await createTestPairs() 144 | await setMarketplaceKind(TEST_DATA.marketplaceId, MarketplaceKind.Public, destAccount, WaitUntil.BlockInclusion) 145 | const nEvent = await createNft( 146 | "Testing - Create an NFT from SDK for listing", 147 | 0, 148 | undefined, 149 | false, 150 | destAccount, 151 | WaitUntil.BlockInclusion, 152 | ) 153 | TEST_DATA.nftId = nEvent.nftId 154 | const mpEvent = await listNft(TEST_DATA.nftId, TEST_DATA.marketplaceId, 10, destAccount, WaitUntil.BlockInclusion) 155 | const nData = await getNftData(TEST_DATA.nftId) 156 | const price = numberToBalance(10).toString() 157 | expect( 158 | mpEvent.nftId === TEST_DATA.nftId && 159 | mpEvent.marketplaceId === TEST_DATA.marketplaceId && 160 | mpEvent.price === price && 161 | mpEvent.priceRounded === 10 && 162 | mpEvent.commissionFee === "10" && 163 | mpEvent.commissionFeeRounded === 10 && 164 | mpEvent.commissionFeeType === MarketplaceConfigFeeType.Percentage && 165 | nData?.state.isListed, 166 | ).toBe(true) 167 | }) 168 | 169 | it("Testing to Unlist an NFT on a marketplace", async (): Promise => { 170 | const { dest: destAccount } = await createTestPairs() 171 | const mpEvent = await unlistNft(TEST_DATA.nftId, destAccount, WaitUntil.BlockInclusion) 172 | const nData = await getNftData(TEST_DATA.nftId) 173 | expect(mpEvent.nftId === TEST_DATA.nftId && nData?.state.isListed).toBe(false) 174 | }) 175 | 176 | it("Testing to Buy an NFT from a marketplace", async (): Promise => { 177 | const { test: testAccount, dest: destAccount } = await createTestPairs() 178 | await listNft(TEST_DATA.nftId, TEST_DATA.marketplaceId, 10, destAccount, WaitUntil.BlockInclusion) 179 | const mpEvent = await buyNft(TEST_DATA.nftId, 10, testAccount, WaitUntil.BlockInclusion) 180 | const nData = await getNftData(TEST_DATA.nftId) 181 | const listedPrice = numberToBalance(10).toString() 182 | const marketplaceCut = numberToBalance(1).toString() 183 | const royaltyCut = numberToBalance(0).toString() 184 | expect( 185 | mpEvent.nftId === TEST_DATA.nftId && 186 | mpEvent.marketplaceId === TEST_DATA.marketplaceId && 187 | mpEvent.buyer === testAccount.address && 188 | mpEvent.listedPrice === listedPrice && 189 | mpEvent.listedPriceRounded === 10 && 190 | mpEvent.marketplaceCut === marketplaceCut && 191 | mpEvent.marketplaceCutRounded === 1 && 192 | mpEvent.royaltyCut === royaltyCut && 193 | mpEvent.royaltyCutRounded === 0 && 194 | nData?.owner === testAccount.address, 195 | ).toBe(true) 196 | }) 197 | }) 198 | --------------------------------------------------------------------------------