diff --git a/.github/workflows/be-api-v2-tests.yml b/.github/workflows/be-api-v2-tests.yml new file mode 100644 index 0000000..5689908 --- /dev/null +++ b/.github/workflows/be-api-v2-tests.yml @@ -0,0 +1,36 @@ +name: Block Explorer API v2 Tests + +on: + push: + branches: [ main, development, development/** ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build packages + run: npm run build + + - name: Run Block Explorer API v2 Tests + run: | + cd packages/dag4-network + npm test -- --grep "BlockExplorerV2Api" --timeout 60000 + env: + CI: true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0c5db25..4c831f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -164,6 +164,18 @@ "node": ">=4" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@hutson/parse-repository-url": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", @@ -173,6 +185,31 @@ "node": ">=6.9.0" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@metamask/obs-store": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@metamask/obs-store/-/obs-store-6.0.2.tgz", @@ -334,6 +371,30 @@ "resolved": "packages/dag4-wallet", "link": true }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/bn.js": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", @@ -465,6 +526,30 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk/node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", @@ -8969,6 +9054,7 @@ "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9065,6 +9151,12 @@ "uuid": "bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -9665,9 +9757,82 @@ "dependencies": { "@stardust-collective/dag4-core": "2.4.0", "bignumber.js": "^9.0.1", - "rxjs": "^6.6.3" + "rxjs": "^6.6.3", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/chai": "^4.2.12", + "@types/mocha": "^8.2.3", + "chai": "^4.2.0", + "mocha": "^8.1.3", + "node-fetch": "^2.6.7", + "ts-node": "^10.9.1", + "typescript": "^4.6.4" + } + }, + "packages/dag4-network/node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "packages/dag4-network/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "packages/dag4-network/node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" }, - "devDependencies": {} + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } }, "packages/dag4-wallet": { "name": "@stardust-collective/dag4-wallet", @@ -9679,11 +9844,11 @@ "@stardust-collective/dag4-network": "2.4.0", "bignumber.js": "^9.0.1", "rimraf": "^3.0.2", - "typescript": "^4.6.4", "zod": "^3.24.2" }, "devDependencies": { - "node-localstorage": "^2.1.6" + "node-localstorage": "^2.1.6", + "typescript": "^4.6.4" } }, "packages/dag4-xchain-ethereum": { @@ -9830,12 +9995,43 @@ } } }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, "@hutson/parse-repository-url": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", "dev": true }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@metamask/obs-store": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@metamask/obs-store/-/obs-store-6.0.2.tgz", @@ -10072,8 +10268,51 @@ "version": "file:packages/dag4-network", "requires": { "@stardust-collective/dag4-core": "2.4.0", + "@types/chai": "^4.2.12", + "@types/mocha": "^8.2.3", "bignumber.js": "^9.0.1", - "rxjs": "^6.6.3" + "chai": "^4.2.0", + "mocha": "^8.1.3", + "node-fetch": "^2.6.7", + "rxjs": "^6.6.3", + "ts-node": "^10.9.1", + "typescript": "^4.6.4", + "zod": "^3.24.2" + }, + "dependencies": { + "acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + } } }, "@stardust-collective/dag4-wallet": { @@ -10089,6 +10328,30 @@ "zod": "^3.24.2" } }, + "@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "@types/bn.js": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz", @@ -10210,6 +10473,23 @@ "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", "dev": true }, + "acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "requires": { + "acorn": "^8.11.0" + }, + "dependencies": { + "acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true + } + } + }, "add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", @@ -16762,7 +17042,8 @@ "typescript": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", - "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==" + "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==", + "dev": true }, "uglify-js": { "version": "3.15.5", @@ -16833,6 +17114,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/packages/dag4-network/package.json b/packages/dag4-network/package.json index 59b13d8..e20993c 100644 --- a/packages/dag4-network/package.json +++ b/packages/dag4-network/package.json @@ -14,6 +14,7 @@ "prebuild": "rimraf dist/", "build": "tsc && tsc -p tsconfig.cjs.json ", "rollup": "rollup -c ../../scripts/config/rollup.config.js", + "test": "TS_NODE_PROJECT='tsconfig.cjs.json' mocha -r ts-node/register test/**/*.ts", "prerelease": "npm run build && npm version patch", "release": "node ../../scripts/package/copy && npm run release:npm", "release:npm": "npm publish ./dist -access public", @@ -23,7 +24,16 @@ "dependencies": { "@stardust-collective/dag4-core": "2.4.0", "rxjs": "^6.6.3", - "bignumber.js": "^9.0.1" + "bignumber.js": "^9.0.1", + "zod": "^3.24.2" }, - "devDependencies": {} + "devDependencies": { + "@types/chai": "^4.2.12", + "@types/mocha": "^8.2.3", + "chai": "^4.2.0", + "mocha": "^8.1.3", + "node-fetch": "^2.6.7", + "ts-node": "^10.9.1", + "typescript": "^4.6.4" + } } diff --git a/packages/dag4-network/src/api/v2/block-explorer-api.ts b/packages/dag4-network/src/api/v2/block-explorer-api.ts index 7113bcf..a6c515c 100644 --- a/packages/dag4-network/src/api/v2/block-explorer-api.ts +++ b/packages/dag4-network/src/api/v2/block-explorer-api.ts @@ -3,14 +3,15 @@ import {DNC} from '../../DNC'; import { SnapshotV2, TransactionV2, - GetTransactionResponseV2, RewardTransaction, AddressBalanceV2, BlockV2, - CurrencySnapshot + CurrencySnapshotV2 } from '../../dto/v2'; type HashOrOrdinal = string | number; +export type Response = { data: T }; +export type ResponseWithMetadata = Response & { meta?: { next: string } }; export class BlockExplorerV2Api { private service = new RestApi(DNC.BLOCK_EXPLORER_URL); @@ -26,147 +27,156 @@ export class BlockExplorerV2Api { } // Snapshots - async getSnapshot (id: HashOrOrdinal) { - return this.service.$get(`/global-snapshots/${id}`); + async getSnapshot(hash: HashOrOrdinal) { + return this.service.$get>(`/global-snapshots/${hash}`); } - async getTransactionsBySnapshot (id: HashOrOrdinal) { - return this.service.$get(`/global-snapshots/${id}/transactions`); - } - - async getRewardsBySnapshot(id: HashOrOrdinal) { - return this.service.$get(`/global-snapshots/${id}/rewards`); + async getTransactionsBySnapshot (hash: HashOrOrdinal, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); + + return this.service.$get>(`/global-snapshots/${hash}/transactions`, params); } - async getLatestSnapshot () { - return this.service.$get('/global-snapshots/latest'); + async getRewardsBySnapshot(hash: HashOrOrdinal, limit?: number, next?: string) { + const params = this.buildRequestParams({ limit, next }); + + return this.service.$get>(`/global-snapshots/${hash}/rewards`, params); } - async getLatestSnapshotTransactions() { - return this.service.$get('/global-snapshots/latest/transactions'); + async getSnapshots(limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); + + return this.service.$get>(`/global-snapshots`, params); } - async getLatestSnapshotRewards() { - return this.service.$get('/global-snapshots/latest/rewards'); + async getLatestSnapshot () { + return this.service.$get>('/global-snapshots/latest'); } - // Transactions - _formatDate(date: string, paramName: string) { - try { - return new Date(date).toISOString(); - } catch(e) { - throw new Error(`ParamError: "${paramName}" is not valid ISO 8601`); - } + async getLatestSnapshotTransactions(limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); + + return this.service.$get>(`/global-snapshots/latest/transactions`, params); } - _getTransactionSearchPathAndParams (basePath: string, limit, searchAfter, sentOnly, receivedOnly, searchBefore) { - let params, path = basePath; - - if (limit || searchAfter || searchBefore) { + async getLatestSnapshotRewards(limit?: number, next?: string) { + const params = this.buildRequestParams({ limit, next }); + + return this.service.$get>(`/global-snapshots/latest/rewards`, params); + } + + // Private method + private buildRequestParams({ + limit = null, + searchAfter = null, + searchBefore = null, + next = null + } : { + limit?: number, + searchAfter?: string, + searchBefore?: string, + next?: string + }) { + let params; + + if (limit || searchAfter || searchBefore || next) { params = {}; - if (limit > 0) { + if (limit && limit > 0) { params.limit = limit; } + // search_after, search_before and next are mutually exclusive if (searchAfter) { params.search_after = searchAfter; } else if (searchBefore) { params.search_before = searchBefore; + } else if (next) { + params.next = next; } } - if (sentOnly) { - path += '/sent' - } - else if (receivedOnly) { - path += '/received' - } - - return {path, params}; + return params; } - async getTransactions (limit, searchAfter, searchBefore) { - const basePath = `/transactions`; + // Transactions + async getTransactions(limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); - const {path, params} = this._getTransactionSearchPathAndParams(basePath, limit, searchAfter, false, false, searchBefore); - - return this.service.$get(path, params); + return this.service.$get>(`/transactions`, params); } - async getTransactionsByAddress (address: string, limit: number = 0, searchAfter = '', sentOnly = false, receivedOnly = false, searchBefore = '') { - const basePath = `/addresses/${address}/transactions`; + async getTransactionsByAddress(address: string, limit?: number, searchAfter?: string, sentOnly?: boolean, receivedOnly?: boolean, searchBefore?: string, next?: string) { + const searchPath = sentOnly ? '/sent' : receivedOnly ? '/received' : ''; + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); - const {path, params} = this._getTransactionSearchPathAndParams(basePath, limit, searchAfter, sentOnly, receivedOnly, searchBefore); - - return this.service.$get(path, params); + return this.service.$get>(`/addresses/${address}/transactions${searchPath}`, params); } - async getTransaction (hash: string) { - return this.service.$get(`/transactions/${hash}`); + async getTransaction(hash: string) { + return this.service.$get>(`/transactions/${hash}`); } // Addresses - async getAddressBalance(hash: string) { - return this.service.$get(`/addresses/${hash}/balance`); + async getAddressBalance(address: string) { + return this.service.$get>(`/addresses/${address}/balance`); } // Blocks async getCheckpointBlock(hash: string) { - return this.service.$get(`/blocks/${hash}`); + return this.service.$get>(`/blocks/${hash}`); } // Metagraphs async getLatestCurrencySnapshot(metagraphId: string) { - return this.service.$get(`/currency/${metagraphId}/snapshots/latest`); + return this.service.$get>(`/currency/${metagraphId}/snapshots/latest`); } async getCurrencySnapshot(metagraphId: string, hashOrOrdinal: string) { - return this.service.$get(`/currency/${metagraphId}/snapshots/${hashOrOrdinal}`); + return this.service.$get>(`/currency/${metagraphId}/snapshots/${hashOrOrdinal}`); } - async getLatestCurrencySnapshotRewards(metagraphId: string) { - return this.service.$get(`/currency/${metagraphId}/snapshots/latest/rewards`); + async getLatestCurrencySnapshotRewards(metagraphId: string, limit?: number, next?: string) { + const params = this.buildRequestParams({ limit, next }); + + return this.service.$get>(`/currency/${metagraphId}/snapshots/latest/rewards`, params); } - async getCurrencySnapshotRewards(metagraphId: string, hashOrOrdinal: string) { - return this.service.$get(`/currency/${metagraphId}/snapshots/${hashOrOrdinal}/rewards`); + async getCurrencySnapshotRewards(metagraphId: string, hashOrOrdinal: string, limit?: number, next?: string) { + const params = this.buildRequestParams({ limit, next }); + + return this.service.$get>(`/currency/${metagraphId}/snapshots/${hashOrOrdinal}/rewards`, params); } async getCurrencyBlock(metagraphId: string, hash: string) { - return this.service.$get(`/currency/${metagraphId}/blocks/${hash}`); + return this.service.$get>(`/currency/${metagraphId}/blocks/${hash}`); } - async getCurrencyAddressBalance(metagraphId: string, hash: string) { - return this.service.$get(`/currency/${metagraphId}/addresses/${hash}/balance`); + async getCurrencyAddressBalance(metagraphId: string, address: string) { + return this.service.$get>(`/currency/${metagraphId}/addresses/${address}/balance`); } - async getCurrencyTransaction (metagraphId: string, hash: string) { - return this.service.$get(`/currency/${metagraphId}/transactions/${hash}`); + async getCurrencyTransaction(metagraphId: string, hash: string) { + return this.service.$get>(`/currency/${metagraphId}/transactions/${hash}`); } - async getCurrencyTransactions (metagraphId: string, limit, searchAfter, searchBefore) { - const basePath = `/currency/${metagraphId}/transactions`; + async getCurrencyTransactions(metagraphId: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); - const {path, params} = this._getTransactionSearchPathAndParams(basePath, limit, searchAfter, false, false, searchBefore); - - return this.service.$get(path, params); + return this.service.$get>(`/currency/${metagraphId}/transactions`, params); } - async getCurrencyTransactionsByAddress (metagraphId: string, address: string, limit: number = 0, searchAfter = '', sentOnly = false, receivedOnly = false, searchBefore = '') { - const basePath = `/currency/${metagraphId}/addresses/${address}/transactions`; + async getCurrencyTransactionsByAddress(metagraphId: string, address: string, limit?: number, searchAfter?: string, sentOnly?: boolean, receivedOnly?: boolean, searchBefore?: string, next?: string) { + const searchPath = sentOnly ? '/sent' : receivedOnly ? '/received' : ''; + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); - const {path, params} = this._getTransactionSearchPathAndParams(basePath, limit, searchAfter, sentOnly, receivedOnly, searchBefore); - - return this.service.$get(path, params); + return this.service.$get>(`/currency/${metagraphId}/addresses/${address}/transactions${searchPath}`, params); } - async getCurrencyTransactionsBySnapshot (metagraphId: string, hashOrOrdinal: string, limit: number = 0, searchAfter = '', searchBefore = '') { - const basePath = `/currency/${metagraphId}/snapshots/${hashOrOrdinal}/transactions`; + async getCurrencyTransactionsBySnapshot(metagraphId: string, hashOrOrdinal: string, limit?: number, searchAfter?: string, searchBefore?: string, next?: string) { + const params = this.buildRequestParams({ limit, searchAfter, searchBefore, next }); - const {path, params} = this._getTransactionSearchPathAndParams(basePath, limit, searchAfter, false, false, searchBefore); - - return this.service.$get(path, params); + return this.service.$get>(`/currency/${metagraphId}/snapshots/${hashOrOrdinal}/transactions`, params); } } diff --git a/packages/dag4-network/src/dto/v2/address-balance.ts b/packages/dag4-network/src/dto/v2/address-balance.ts index 77c0d20..c4ad62e 100644 --- a/packages/dag4-network/src/dto/v2/address-balance.ts +++ b/packages/dag4-network/src/dto/v2/address-balance.ts @@ -3,6 +3,7 @@ import { SnapshotOrdinal } from './total-supply'; export type AddressBalanceV2 = { balance: number ordinal: number + address: string }; export type L0AddressBalance = { diff --git a/packages/dag4-network/src/dto/v2/block.ts b/packages/dag4-network/src/dto/v2/block.ts index 95e5db4..ce35d2a 100644 --- a/packages/dag4-network/src/dto/v2/block.ts +++ b/packages/dag4-network/src/dto/v2/block.ts @@ -1,19 +1,15 @@ -// TODO: confirm through API, not defined in API doc -export type HeightV2 = { - min: number - max: number -} export type BlockReference = { hash: string - height: HeightV2 + height: number } export type BlockV2 = { hash: string + height: number + parents: BlockReference[] timestamp: string - snapshot: string - height: HeightV2 transactions: string[] - parent: BlockReference + snapshotHash: string + snapshotOrdinal: number } \ No newline at end of file diff --git a/packages/dag4-network/src/dto/v2/currency-snapshot.ts b/packages/dag4-network/src/dto/v2/currency-snapshot.ts deleted file mode 100644 index 2ce947e..0000000 --- a/packages/dag4-network/src/dto/v2/currency-snapshot.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type CurrencySnapshot = { - hash: string - timestamp: string - ordinal: number - height: number - subHeight: number - lastSnapshotHash: string - blocks: string[] -} diff --git a/packages/dag4-network/src/dto/v2/index.ts b/packages/dag4-network/src/dto/v2/index.ts index feb7119..b03b197 100644 --- a/packages/dag4-network/src/dto/v2/index.ts +++ b/packages/dag4-network/src/dto/v2/index.ts @@ -5,5 +5,4 @@ export * from './address-balance'; export * from './block'; export * from './peer'; export * from './total-supply'; -export * from './currency-snapshot'; export * from './swap-operations'; diff --git a/packages/dag4-network/src/dto/v2/snapshot.ts b/packages/dag4-network/src/dto/v2/snapshot.ts index 93a31b1..11d0484 100644 --- a/packages/dag4-network/src/dto/v2/snapshot.ts +++ b/packages/dag4-network/src/dto/v2/snapshot.ts @@ -2,12 +2,14 @@ import { Proof } from "./transaction"; export type SnapshotV2 = { hash: string; - timestamp: string; ordinal: number; height: number; subHeight: number; lastSnapshotHash: string; blocks: string[]; + epochProgress?: number | null; + timestamp: string; + metagraphSnapshotCount?: number | null; }; export type StateProof = { @@ -66,3 +68,18 @@ export type SnapshotL0 = { value: SnapshotL0Value; proofs: Proof[]; }; + +export type CurrencySnapshotV2 = { + hash: string; + ordinal: number; + height: number; + subHeight: number; + lastSnapshotHash: string; + blocks: string[]; + epochProgress: number; + timestamp: string; + fee?: number | null; + stakingAddress?: string | null; + ownerAddress?: string | null; + sizeInKB?: number | null; +} diff --git a/packages/dag4-network/src/dto/v2/transaction.ts b/packages/dag4-network/src/dto/v2/transaction.ts index 0aa1141..00ee48d 100644 --- a/packages/dag4-network/src/dto/v2/transaction.ts +++ b/packages/dag4-network/src/dto/v2/transaction.ts @@ -16,15 +16,23 @@ type TransactionValueV2 = { export type TransactionV2 = { hash: string + ordinal: number source: string destination: string amount: number fee: number - parent: TransactionReference - snapshot: string - block: string + parent: TransactionReference + salt: number + blockHash: string + snapshotHash: string + snapshotOrdinal: number + transactionOriginal?: { + value: TransactionValueV2, + proofs: Proof[] + } | null timestamp: string - transactionOriginal: TransactionReference + globalSnapshotHash: string + globalSnapshotOrdinal: number } export type PendingTransaction = { @@ -46,7 +54,3 @@ export type PostTransactionV2 = { export type PostTransactionResponseV2 = { hash: string }; - -export type GetTransactionResponseV2 = { - data: TransactionV2 -} \ No newline at end of file diff --git a/packages/dag4-network/src/metagraph-token-network.ts b/packages/dag4-network/src/metagraph-token-network.ts index ad3cdac..89b04e6 100644 --- a/packages/dag4-network/src/metagraph-token-network.ts +++ b/packages/dag4-network/src/metagraph-token-network.ts @@ -3,7 +3,7 @@ import { PostTransactionV2, PendingTransaction, TransactionV2, - CurrencySnapshot + CurrencySnapshotV2 } from "./dto/v2"; import {BlockExplorerV2Api} from './api/v2/block-explorer-api'; import { MetagraphTokenL0Api } from "./api/metagraph-token/l0-api"; @@ -87,7 +87,7 @@ class MetagraphTokenNetwork { return response.data ? response.data.hash : response.hash; } - async getLatestSnapshot(): Promise { + async getLatestSnapshot(): Promise { const response = (await this.beApi.getLatestCurrencySnapshot(this.connectedNetwork.metagraphId)) as any; return response.data; diff --git a/packages/dag4-network/test/be-api-v2.schemas.ts b/packages/dag4-network/test/be-api-v2.schemas.ts new file mode 100644 index 0000000..827e1ad --- /dev/null +++ b/packages/dag4-network/test/be-api-v2.schemas.ts @@ -0,0 +1,130 @@ +import { z } from 'zod'; + +// Helper schemas for nested types +export const TransactionReferenceSchema = z.object({ + hash: z.string(), + ordinal: z.number() +}); + +export const BlockReferenceSchema = z.object({ + hash: z.string(), + height: z.number() +}); + +export const ProofSchema = z.object({ + signature: z.string(), + id: z.string() +}); + +export const TransactionValueV2Schema = z.object({ + source: z.string(), + destination: z.string(), + amount: z.number(), + fee: z.number(), + parent: TransactionReferenceSchema, + salt: z.union([z.string(), z.number()]) // BigNumber or string +}); + +// Core data type schemas +export const TransactionV2Schema = z.object({ + hash: z.string(), + ordinal: z.number(), + source: z.string(), + destination: z.string(), + amount: z.number(), + fee: z.number(), + parent: TransactionReferenceSchema, + salt: z.number(), + blockHash: z.string(), + snapshotHash: z.string(), + snapshotOrdinal: z.number(), + transactionOriginal: z.object({ + value: TransactionValueV2Schema, + proofs: z.array(ProofSchema) + }).nullable(), + timestamp: z.string(), + globalSnapshotHash: z.string(), + globalSnapshotOrdinal: z.number() +}); + +export const SnapshotV2Schema = z.object({ + hash: z.string(), + ordinal: z.number(), + height: z.number(), + subHeight: z.number(), + lastSnapshotHash: z.string(), + blocks: z.array(z.string()), + epochProgress: z.number().nullable().optional(), + timestamp: z.string(), + metagraphSnapshotCount: z.number().nullable().optional() +}); + +export const RewardTransactionSchema = z.object({ + destination: z.string(), + amount: z.number() +}); + +export const AddressBalanceV2Schema = z.object({ + balance: z.number(), + address: z.string(), + ordinal: z.number() +}); + +export const CurrencySnapshotV2Schema = z.object({ + hash: z.string(), + ordinal: z.number(), + height: z.number(), + subHeight: z.number(), + lastSnapshotHash: z.string(), + blocks: z.array(z.string()), + epochProgress: z.number(), + timestamp: z.string(), + fee: z.number().nullable().optional(), + stakingAddress: z.string().nullable().optional(), + ownerAddress: z.string().nullable().optional(), + sizeInKB: z.number().nullable().optional() +}); + +export const BlockV2Schema = z.object({ + hash: z.string(), + height: z.number(), + parents: z.array(BlockReferenceSchema), + timestamp: z.string(), + transactions: z.array(z.string()), + snapshotHash: z.string(), + snapshotOrdinal: z.number() +}); + +// Response wrapper schemas +export const ResponseMetadataSchema = z.object({ + next: z.string() +}).optional(); + +export const ResponseSchema = (dataSchema: T) => z.object({ + data: z.union([ + dataSchema, + z.array(dataSchema), + z.null() + ]) +}); + +export const ResponseWithMetadataSchema = (dataSchema: T) => z.object({ + data: z.union([ + dataSchema, + z.array(dataSchema), + z.null() + ]), + meta: ResponseMetadataSchema +}); + +// Schema mapping for easy access +export const DATA_SCHEMAS = { + TransactionV2: TransactionV2Schema, + SnapshotV2: SnapshotV2Schema, + RewardTransaction: RewardTransactionSchema, + AddressBalanceV2: AddressBalanceV2Schema, + CurrencySnapshotV2: CurrencySnapshotV2Schema, + BlockV2: BlockV2Schema +} as const; + +export type DataSchemaKeys = keyof typeof DATA_SCHEMAS; \ No newline at end of file diff --git a/packages/dag4-network/test/be-api-v2.test.ts b/packages/dag4-network/test/be-api-v2.test.ts new file mode 100644 index 0000000..b20409a --- /dev/null +++ b/packages/dag4-network/test/be-api-v2.test.ts @@ -0,0 +1,419 @@ +import { expect } from 'chai'; +import { z } from 'zod'; +import { BlockExplorerV2Api, type ResponseWithMetadata, type Response } from '../src/api/v2/block-explorer-api'; +import { crossPlatformDi } from '@stardust-collective/dag4-core'; +import { FetchRestService } from '@stardust-collective/dag4-core/dist/cjs/cross-platform/clients/fetch.http'; +import type { AddressBalanceV2, CurrencySnapshotV2, RewardTransaction, SnapshotV2, TransactionV2 } from '../src/dto/v2'; +import { + DATA_SCHEMAS, + type DataSchemaKeys, + ResponseSchema, + ResponseWithMetadataSchema +} from './be-api-v2.schemas'; + +// Initialize DI system for Node.js environment +const fetch = require('node-fetch'); +const fetchRestService = new FetchRestService(fetch); +crossPlatformDi.registerHttpClient(fetchRestService); + +// Test constants +const MAINNET_URL = 'https://be-mainnet.constellationnetwork.io'; +const DAG_ADDRESS = 'DAG77zerQ2BUVhtVgkmseihkEfLXieBBm57vqA4J'; +const METAGRAPH_ID = 'DAG0CyySf35ftDQDQBnd1bdQ9aPyUdacMghpnCuM'; // DOR metagraph on Mainnet +const LIMIT = 5; +const TIMEOUT = 60000; + +// Hashes storage +let HASHES: { + snapshotHash?: string; + transactionHash?: string; + currencySnapshotHash?: string; +} = {}; + + +/** + * Validates that a response has the expected Response structure using Zod + */ +function validateResponse(result: any, schemaKey?: DataSchemaKeys): asserts result is Response { + expect(result).to.exist; + expect(result).to.have.property('data'); + + if (!schemaKey) { + return; + } + + // Handle edge cases that should skip validation + if (result.data === null) { + console.warn('API returned null data, skipping detailed validation'); + return; + } + + if (Array.isArray(result.data) && result.data.length === 0) { + console.warn('API returned empty array, skipping detailed validation'); + return; + } + + const dataSchema = DATA_SCHEMAS[schemaKey]; + const responseSchema = ResponseSchema(dataSchema); + + try { + responseSchema.parse(result); + } catch (error) { + if (error instanceof z.ZodError) { + console.error(`Response validation failed for ${schemaKey}:`, error.errors); + throw new Error(`Invalid Response<${schemaKey}> structure: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); + } + throw error; + } +} + +/** + * Validates that a response has the expected ResponseWithMetadata structure using Zod + */ +function validateResponseWithMetadata(result: any, schemaKey?: DataSchemaKeys): asserts result is ResponseWithMetadata { + expect(result).to.exist; + expect(result).to.have.property('data'); + + if (!schemaKey) { + return; + } + + // Handle edge cases that should skip validation + if (result.data === null) { + console.warn('API returned null data, skipping detailed validation'); + return; + } + + if (Array.isArray(result.data) && result.data.length === 0) { + console.warn('API returned empty array, skipping detailed validation'); + return; + } + + const dataSchema = DATA_SCHEMAS[schemaKey]; + const responseSchema = ResponseWithMetadataSchema(dataSchema); + + try { + responseSchema.parse(result); + } catch (error) { + if (error instanceof z.ZodError) { + console.error(`ResponseWithMetadata validation failed for ${schemaKey}:`, error.errors); + throw new Error(`Invalid ResponseWithMetadata<${schemaKey}> structure: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); + } + throw error; + } +} + +/** + * Fetch hashes for tests + */ +async function fetchHashes(api: BlockExplorerV2Api): Promise { + console.log('Fetching hashes for tests...'); + + try { + // Fetch snapshot hash + const snapshotsResult = await api.getSnapshots(1); + if (snapshotsResult.data && snapshotsResult.data.length > 0) { + HASHES.snapshotHash = snapshotsResult.data[0].hash; + console.log('Snapshot hash:', HASHES.snapshotHash); + } + + // Fetch transaction hash + const transactionsResult = await api.getTransactions(1); + if (transactionsResult.data && transactionsResult.data.length > 0) { + HASHES.transactionHash = transactionsResult.data[0].hash; + console.log('Transaction hash:', HASHES.transactionHash); + } + + // Fetch currency snapshot hash + const latestCurrencySnapshot = await api.getLatestCurrencySnapshot(METAGRAPH_ID); + if (latestCurrencySnapshot.data && !Array.isArray(latestCurrencySnapshot.data)) { + HASHES.currencySnapshotHash = latestCurrencySnapshot.data.hash; + console.log('Currency snapshot hash:', HASHES.currencySnapshotHash); + } + + } catch (error) { + console.warn('Error fetching hashes:', error); + } +} + +describe('BlockExplorerV2Api', function() { + this.timeout(TIMEOUT); + + let api: BlockExplorerV2Api; + + before(async () => { + api = new BlockExplorerV2Api(MAINNET_URL); + await fetchHashes(api); + }); + + describe('Snapshot Methods', () => { + describe('getSnapshot', () => { + it('should get snapshot by hash', async function() { + if (!HASHES.snapshotHash) { + this.skip(); + } + const result = await api.getSnapshot(HASHES.snapshotHash); + validateResponse(result, 'SnapshotV2'); + }); + + it('should throw error for invalid hash', async () => { + try { + await api.getSnapshot('invalid-hash'); + } catch (error) { + expect(error).to.exist; + } + }); + }); + + describe('getSnapshots', () => { + it('should get snapshots', async () => { + const result = await api.getSnapshots(LIMIT); + validateResponseWithMetadata(result, 'SnapshotV2'); + }); + }); + + describe('getLatestSnapshot', () => { + it('should get latest snapshot', async () => { + const result = await api.getLatestSnapshot(); + validateResponse(result, 'SnapshotV2'); + }); + }); + + describe('getTransactionsBySnapshot', () => { + it('should get transactions by snapshot', async function() { + if (!HASHES.snapshotHash) { + this.skip(); + } + const result = await api.getTransactionsBySnapshot(HASHES.snapshotHash, LIMIT); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + }); + + describe('getRewardsBySnapshot', () => { + it('should get rewards by snapshot', async function() { + if (!HASHES.snapshotHash) { + this.skip(); + } + const result = await api.getRewardsBySnapshot(HASHES.snapshotHash, LIMIT); + validateResponseWithMetadata(result, 'RewardTransaction'); + }); + }); + + describe('getLatestSnapshotTransactions', () => { + it('should get latest snapshot transactions', async () => { + const result = await api.getLatestSnapshotTransactions(LIMIT); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + }); + + describe('getLatestSnapshotRewards', () => { + it('should get latest snapshot rewards', async () => { + const result = await api.getLatestSnapshotRewards(LIMIT); + validateResponseWithMetadata(result, 'RewardTransaction'); + }); + }); + }); + + describe('Transaction Methods', () => { + describe('getTransactions', () => { + it('should get transactions without parameters', async () => { + const result = await api.getTransactions(); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + + it('should get transactions with limit', async () => { + const result = await api.getTransactions(LIMIT); + validateResponseWithMetadata(result, 'TransactionV2'); + if (Array.isArray(result.data)) { + expect(result.data.length).to.be.at.most(LIMIT); + } + }); + + it('should handle search_after parameter', async function() { + const initialResult = await api.getTransactions(LIMIT); + if (Array.isArray(initialResult.data) && initialResult.data.length > 2) { + const searchAfter = initialResult.data[2].hash; + const result = await api.getTransactions(LIMIT, searchAfter); + validateResponseWithMetadata(result, 'TransactionV2'); + } else { + this.skip(); + } + }); + + it('should handle search_before parameter', async function() { + const initialResult = await api.getTransactions(LIMIT); + if (Array.isArray(initialResult.data) && initialResult.data.length > 2) { + const searchBefore = initialResult.data[2].hash; + const result = await api.getTransactions(LIMIT, undefined, searchBefore); + validateResponseWithMetadata(result, 'TransactionV2'); + } else { + this.skip(); + } + }); + + it('should handle next parameter', async function() { + const initialResult = await api.getTransactions(LIMIT); + if (Array.isArray(initialResult.data) && initialResult.data.length > 0 && initialResult.meta?.next) { + const next = initialResult.meta.next; + const result = await api.getTransactions(LIMIT, undefined, undefined, next); + validateResponseWithMetadata(result, 'TransactionV2'); + } else { + this.skip(); + } + }); + + it('should handle error limit', async () => { + try { + await api.getTransactions(0); + } catch (error) { + expect(error).to.exist; + } + }); + }); + + describe('getTransaction', () => { + it('should get transaction by hash', async function() { + if (!HASHES.transactionHash) { + this.skip(); + } + const result = await api.getTransaction(HASHES.transactionHash); + validateResponse(result, 'TransactionV2'); + }); + + it('should throw error for invalid hash', async () => { + try { + await api.getTransaction('invalid-hash'); + } catch (error) { + expect(error).to.exist; + } + }); + }); + + describe('getTransactionsByAddress', () => { + it('should get transactions by address', async () => { + const result = await api.getTransactionsByAddress(DAG_ADDRESS, LIMIT); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + + it('should get sent transactions by address', async () => { + const result = await api.getTransactionsByAddress(DAG_ADDRESS, LIMIT, undefined, true, false); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + + it('should get received transactions by address', async () => { + const result = await api.getTransactionsByAddress(DAG_ADDRESS, LIMIT, undefined, false, true); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + }); + }); + + describe('Address Methods', () => { + describe('getAddressBalance', () => { + it('should get address balance', async () => { + const result = await api.getAddressBalance(DAG_ADDRESS); + validateResponse(result, 'AddressBalanceV2'); + }); + + it('should throw error for invalid address', async () => { + try { + await api.getAddressBalance('invalid-address'); + } catch (error) { + expect(error).to.exist; + } + }); + }); + }); + + describe('Metagraph Methods', () => { + describe('getLatestCurrencySnapshot', () => { + it('should get latest currency snapshot', async () => { + const result = await api.getLatestCurrencySnapshot(METAGRAPH_ID); + validateResponse(result, 'CurrencySnapshotV2'); + }); + }); + + describe('getCurrencySnapshot', () => { + it('should get currency snapshot by hash', async function() { + if (!HASHES.currencySnapshotHash) { + this.skip(); + } + const result = await api.getCurrencySnapshot(METAGRAPH_ID, HASHES.currencySnapshotHash); + validateResponse(result, 'CurrencySnapshotV2'); + }); + }); + + describe('getLatestCurrencySnapshotRewards', () => { + it('should get latest currency snapshot rewards', async () => { + const result = await api.getLatestCurrencySnapshotRewards(METAGRAPH_ID, LIMIT); + validateResponseWithMetadata(result, 'RewardTransaction'); + }); + }); + + describe('getCurrencySnapshotRewards', () => { + it('should get currency snapshot rewards', async function() { + if (!HASHES.currencySnapshotHash) { + this.skip(); + } + const result = await api.getCurrencySnapshotRewards(METAGRAPH_ID, HASHES.currencySnapshotHash, LIMIT); + validateResponseWithMetadata(result, 'RewardTransaction'); + }); + }); + + describe('getCurrencyAddressBalance', () => { + it('should get currency address balance', async () => { + const result = await api.getCurrencyAddressBalance(METAGRAPH_ID, DAG_ADDRESS); + validateResponse(result, 'AddressBalanceV2'); + }); + }); + + describe('getCurrencyTransaction', () => { + it('should get currency transaction', async function() { + if (!HASHES.transactionHash) { + this.skip(); + } + try { + const result = await api.getCurrencyTransaction(METAGRAPH_ID, HASHES.transactionHash); + validateResponse(result, 'TransactionV2'); + } catch (error) { + // Transaction may not exist in currency, that's ok + expect(error).to.exist; + } + }); + }); + + describe('getCurrencyTransactions', () => { + it('should get currency transactions', async () => { + const result = await api.getCurrencyTransactions(METAGRAPH_ID, LIMIT); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + }); + + describe('getCurrencyTransactionsByAddress', () => { + it('should get currency transactions by address', async () => { + const result = await api.getCurrencyTransactionsByAddress(METAGRAPH_ID, DAG_ADDRESS, LIMIT); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + + it('should get sent currency transactions by address', async () => { + const result = await api.getCurrencyTransactionsByAddress(METAGRAPH_ID, DAG_ADDRESS, LIMIT, undefined, true, false); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + + it('should get received currency transactions by address', async () => { + const result = await api.getCurrencyTransactionsByAddress(METAGRAPH_ID, DAG_ADDRESS, LIMIT, undefined, false, true); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + }); + + describe('getCurrencyTransactionsBySnapshot', () => { + it('should get currency transactions by snapshot', async function() { + if (!HASHES.currencySnapshotHash) { + this.skip(); + } + const result = await api.getCurrencyTransactionsBySnapshot(METAGRAPH_ID, HASHES.currencySnapshotHash, LIMIT); + validateResponseWithMetadata(result, 'TransactionV2'); + }); + }); + }); +}); + diff --git a/packages/dag4-wallet/package.json b/packages/dag4-wallet/package.json index dc6ff69..36bb270 100644 --- a/packages/dag4-wallet/package.json +++ b/packages/dag4-wallet/package.json @@ -26,10 +26,10 @@ "@stardust-collective/dag4-network": "2.4.0", "bignumber.js": "^9.0.1", "rimraf": "^3.0.2", - "typescript": "^4.6.4", "zod": "^3.24.2" }, "devDependencies": { - "node-localstorage": "^2.1.6" + "node-localstorage": "^2.1.6", + "typescript": "^4.6.4" } } diff --git a/packages/dag4-wallet/src/dag-monitor.ts b/packages/dag4-wallet/src/dag-monitor.ts index e109529..91d6743 100644 --- a/packages/dag4-wallet/src/dag-monitor.ts +++ b/packages/dag4-wallet/src/dag-monitor.ts @@ -123,10 +123,14 @@ export class DagMonitor { amount, fee, parent: { ordinal, hash: '' }, - snapshot: '', - block: '', + snapshotHash: '', + blockHash: '', timestamp: new Date(timestamp).toISOString(), - transactionOriginal: { ordinal, hash }, + transactionOriginal: { value: {}, proofs: [] }, + snapshotOrdinal: 0, + globalSnapshotHash: '', + globalSnapshotOrdinal: 0, + salt: 0, } as TransactionV2; }