diff --git a/common/changes/@cityofzion/neon-dappkit/CU-86drumcht_2024-04-01-17-48.json b/common/changes/@cityofzion/neon-dappkit/CU-86drumcht_2024-04-01-17-48.json new file mode 100644 index 0000000..9e6a565 --- /dev/null +++ b/common/changes/@cityofzion/neon-dappkit/CU-86drumcht_2024-04-01-17-48.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@cityofzion/neon-dappkit", + "comment": "", + "type": "none" + } + ], + "packageName": "@cityofzion/neon-dappkit" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index e0e3c35..1ed60fb 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -11,17 +11,21 @@ importers: '@cityofzion/neon-dappkit-types': workspace:* '@cityofzion/neon-js': 5.5.1 '@istanbuljs/nyc-config-typescript': ^1.0.2 + '@types/adm-zip': 0.5.5 '@types/elliptic': 6.4.14 '@types/expect': ^24.3.0 + '@types/follow-redirects': ^1.14.4 '@types/mocha': 10.0.0 '@types/node': ^18.14.6 '@typescript-eslint/eslint-plugin': ^6.5.0 '@typescript-eslint/parser': ^6.5.0 + adm-zip: ^0.5.10 chai: ~4.3.7 crypto-browserify: ^3.12.0 crypto-js: ^4.1.1 elliptic: ^6.5.4 eslint: ^8.48.0 + follow-redirects: ^1.14.4 mocha: ^10.0.0 nyc: ^15.1.0 randombytes: ^2.1.0 @@ -29,6 +33,7 @@ importers: stream: ^0.0.2 stream-browserify: ^3.0.0 ts-mocha: ^10.0.0 + ts-node: ^10.9.2 typescript: ^4.3.2 dependencies: '@cityofzion/neon-dappkit-types': link:../neon-dappkit-types @@ -43,17 +48,22 @@ importers: devDependencies: '@cityofzion/neon-core': 5.5.1 '@istanbuljs/nyc-config-typescript': 1.0.2_nyc@15.1.0 + '@types/adm-zip': 0.5.5 '@types/elliptic': 6.4.14 '@types/expect': 24.3.0 + '@types/follow-redirects': 1.14.4 '@types/mocha': 10.0.0 '@types/node': 18.18.6 '@typescript-eslint/eslint-plugin': 6.8.0_ga4p3v3rgh7x375g7wjufee6mi '@typescript-eslint/parser': 6.8.0_o3et2ndnedfdhen34uq7t66m3y + adm-zip: 0.5.12 chai: 4.3.10 eslint: 8.51.0 + follow-redirects: 1.15.6 mocha: 10.2.0 nyc: 15.1.0 ts-mocha: 10.0.0_mocha@10.2.0 + ts-node: 10.9.2_ljq6rywul5eez5wdg7yw42blvy typescript: 4.9.5 ../../packages/neon-dappkit-types: @@ -308,6 +318,13 @@ packages: - encoding dev: false + /@cspotcode/source-map-support/0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + /@eslint-community/eslint-utils/4.4.0_eslint@8.51.0: resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -447,6 +464,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@jridgewell/trace-mapping/0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@noble/curves/1.0.0: resolution: {integrity: sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==} dependencies: @@ -496,6 +520,28 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@tsconfig/node10/1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + dev: true + + /@tsconfig/node12/1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14/1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16/1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/adm-zip/0.5.5: + resolution: {integrity: sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw==} + dependencies: + '@types/node': 18.18.6 + dev: true + /@types/bn.js/5.1.3: resolution: {integrity: sha512-wT1B4iIO82ecXkdN6waCK8Ou7E71WU+mP1osDA5Q8c6Ur+ozU2vIKUIhSpUr6uE5L2YHocKS1Z2jG2fBC1YVeg==} dependencies: @@ -515,6 +561,12 @@ packages: expect: 29.7.0 dev: true + /@types/follow-redirects/1.14.4: + resolution: {integrity: sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==} + dependencies: + '@types/node': 18.18.6 + dev: true + /@types/istanbul-lib-coverage/2.0.5: resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==} dev: true @@ -712,12 +764,22 @@ packages: acorn: 8.10.0 dev: true + /acorn-walk/8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + /acorn/8.10.0: resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} hasBin: true dev: true + /adm-zip/0.5.12: + resolution: {integrity: sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==} + engines: {node: '>=6.0'} + dev: true + /aggregate-error/3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -787,6 +849,10 @@ packages: resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} dev: true + /arg/4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + /argparse/1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -1125,6 +1191,10 @@ packages: sha.js: 2.4.11 dev: false + /create-require/1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + /cross-fetch/3.1.8: resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} dependencies: @@ -1230,6 +1300,11 @@ packages: engines: {node: '>=0.3.1'} dev: true + /diff/4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + /diff/5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} @@ -1524,6 +1599,16 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /follow-redirects/1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: true + /foreground-child/2.0.0: resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} engines: {node: '>=8.0.0'} @@ -2730,6 +2815,37 @@ packages: tsconfig-paths: 3.14.2 dev: true + /ts-node/10.9.2_ljq6rywul5eez5wdg7yw42blvy: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.18.6 + acorn: 8.10.0 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-node/7.0.1: resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==} engines: {node: '>=4.2.0'} @@ -2830,6 +2946,10 @@ packages: hasBin: true dev: true + /v8-compile-cache-lib/3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + /vscode-oniguruma/1.7.0: resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} dev: true @@ -2969,6 +3089,11 @@ packages: engines: {node: '>=4'} dev: true + /yn/3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/packages/neon-dappkit/data/protocol.unit_testnet.single.yml b/packages/neon-dappkit/data/protocol.unit_testnet.single.yml index 1ebda60..3ebfe0a 100644 --- a/packages/neon-dappkit/data/protocol.unit_testnet.single.yml +++ b/packages/neon-dappkit/data/protocol.unit_testnet.single.yml @@ -1,7 +1,7 @@ ProtocolConfiguration: Magic: 42 MaxTraceableBlocks: 200000 - TimePerBlock: 100ms + TimePerBlock: 300ms MemPoolSize: 100 StandbyCommittee: - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 @@ -16,7 +16,7 @@ ApplicationConfiguration: # LogPath could be set up in case you need stdout logs to some proper file. # LogPath: "./log/neogo.log" DBConfiguration: - Type: "inmemory" #other options: 'inmemory','boltdb' + Type: 'inmemory' #other options: 'inmemory','boltdb' # DB type options. Uncomment those you need in case you want to switch DB type. # LevelDBOptions: # DataDirectoryPath: "./chains/unit_testnet" @@ -24,7 +24,7 @@ ApplicationConfiguration: # FilePath: "./chains/unit_testnet.bolt" P2P: Addresses: - - ":0" # in form of "[host]:[port][:announcedPort]" + - ':0' # in form of "[host]:[port][:announcedPort]" DialTimeout: 3s ProtoTickInterval: 2s PingInterval: 30s @@ -36,19 +36,19 @@ ApplicationConfiguration: Consensus: Enabled: true UnlockWallet: - Path: "wallet1_solo.json" - Password: "one" + Path: 'wallet1_solo.json' + Password: 'one' RPC: MaxGasInvoke: 15 Enabled: true Addresses: - - "127.0.0.1:0" # let the system choose port dynamically + - '127.0.0.1:50012' EnableCORSWorkaround: false Prometheus: Enabled: false #since it's not useful for unit tests. Addresses: - - ":2112" + - ':2112' Pprof: Enabled: false #since it's not useful for unit tests. Addresses: - - ":2113" + - ':2113' diff --git a/packages/neon-dappkit/package.json b/packages/neon-dappkit/package.json index 8c7ce2a..e0e2bb2 100644 --- a/packages/neon-dappkit/package.json +++ b/packages/neon-dappkit/package.json @@ -12,7 +12,7 @@ "lint": "eslint .", "format": "eslint --fix", "test": "ts-node test/setup-neo-go.ts && ts-mocha --reporter json > ../../mocha-results.json src/**/*.spec.ts", - "test-print": "ts-mocha src/**/*.spec.ts", + "test-print": "ts-node test/setup-neo-go.ts && ts-mocha src/**/*.spec.ts", "coverage": "nyc pnpm test" }, "dependencies": { @@ -29,6 +29,7 @@ "devDependencies": { "@cityofzion/neon-core": "5.5.1", "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/adm-zip": "0.5.5", "@types/elliptic": "6.4.14", "@types/expect": "^24.3.0", "@types/follow-redirects": "^1.14.4", diff --git a/packages/neon-dappkit/src/NeonEventListener.spec.ts b/packages/neon-dappkit/src/NeonEventListener.spec.ts index 84ced27..9868e19 100644 --- a/packages/neon-dappkit/src/NeonEventListener.spec.ts +++ b/packages/neon-dappkit/src/NeonEventListener.spec.ts @@ -1,6 +1,15 @@ import { ChildProcess, spawn } from 'child_process' -import * as path from 'path'; +import { NeonEventListener, NeonInvoker, NeonParser } from './index' +import * as path from 'path' import assert from 'assert' +import { + ContractInvocationMulti, + Neo3ApplicationLog, + Neo3EventListenerCallback, + Neo3EventWithState, + TypeChecker, +} from '@cityofzion/neon-dappkit-types' +import { wallet } from '@cityofzion/neon-core' function wait(ms: number) { return new Promise((resolve) => { @@ -18,23 +27,432 @@ function getDataDir() { describe('NeonEventListener', function () { this.timeout(60000) - let childProcess: ChildProcess; + let childProcess: ChildProcess + const rpcAddress = 'http://127.0.0.1:50012' + const eventListener = new NeonEventListener(rpcAddress, { + waitForApplicationLog: { maxAttempts: 10, waitMs: 100 }, + waitForEventMs: 100, + }) + const gasScriptHash = '0xd2a4cff31913016155e38e474a2c06d08be276cf' + let accountWithGas: wallet.Account + const waitTime = 900 + + function transferInvocation( + sender: wallet.Account, + receiver: wallet.Account, + amount: string, + ): ContractInvocationMulti { + return { + invocations: [ + { + scriptHash: gasScriptHash, + operation: 'transfer', + args: [ + { type: 'Hash160', value: sender.address }, + { type: 'Hash160', value: receiver.address }, + { type: 'Integer', value: amount }, + { type: 'String', value: 'test' }, + ], + }, + ], + } + } beforeEach(async function () { - const neoGo = neoGoPath(); - const dataDir = getDataDir(); + const neoGo = neoGoPath() + const dataDir = getDataDir() - childProcess = spawn(neoGo, ['node', '--config-file', `${dataDir}/protocol.unit_testnet.single.yml`, '--relative-path', dataDir], {}) - await wait(1200) + childProcess = spawn( + neoGo, + ['node', '--config-file', `${dataDir}/protocol.unit_testnet.single.yml`, '--relative-path', dataDir], + {}, + ) + await wait(waitTime) + + accountWithGas = new wallet.Account( + await wallet.decrypt('6PYM8VdX3hY4B51UJxmm8D41RQMbpJT8aYHibyQ67gjkUPmvQgu51Y5UQR', 'one', { n: 2, r: 1, p: 1 }), + ) return true }) afterEach('Tear down', async function () { - return childProcess.kill(); + return childProcess.kill() }) it('does execute neoGo', async () => { assert(childProcess !== undefined, 'child process running neo-go is set') }) + + it('adds an eventListener', async () => { + const eventName = 'Transfer' + + const eventPromise = new Promise((resolve) => { + const callBack = (notification: Neo3EventWithState) => { + resolve(notification) + + eventListener.removeAllEventListenersOfEvent(gasScriptHash, eventName) + } + + eventListener.addEventListener(gasScriptHash, eventName, callBack) + }) + + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + const notification = await eventPromise + + assert(notification.contract === gasScriptHash, 'Notification should be sent by NeoToken') + assert(notification.eventname === eventName, `Notification should be ${eventName}`) + assert(notification.state !== undefined, 'Notification should return value') + assert(TypeChecker.isStackTypeArray(notification.state), 'Notification value should be an array') + }) + + it('adds eventListeners on the same event', async () => { + const eventName = 'Transfer' + + const eventPromise1 = new Promise((resolve) => { + const callBack1 = (notification: Neo3EventWithState) => { + resolve(notification) + + eventListener.removeAllEventListenersOfEvent(gasScriptHash, eventName) + } + + eventListener.addEventListener(gasScriptHash, eventName, callBack1) + }) + + const eventPromise2 = new Promise((resolve) => { + const callBack2 = (notification: Neo3EventWithState) => { + resolve(notification) + + eventListener.removeAllEventListenersOfEvent(gasScriptHash, eventName) + } + + eventListener.addEventListener(gasScriptHash, eventName, callBack2) + }) + + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + const notification1 = await eventPromise1 + const notification2 = await eventPromise2 + + assert(notification1.contract === notification2.contract, 'Notification contract should be the same') + assert(notification1.eventname === notification2.eventname, 'Notification event name should be the same') + assert(notification1.state === notification2.state, 'Notification state should be the same') + }) + + it('adds eventListener to an event that does not exist', async () => { + eventListener.addEventListener(gasScriptHash, 'DoesNotExist', (arg) => arg) + + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await wait(waitTime) + + eventListener.removeAllEventListenersOfContract(gasScriptHash) + }) + + it('adds eventListener to a smart contract that does not exist', async () => { + const fakeScriptHash = '0x0123456789012345678901234567890123456789' + eventListener.addEventListener(fakeScriptHash, 'DoesNotExist', (arg) => arg) + + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await wait(waitTime) + + eventListener.removeAllEventListenersOfContract(fakeScriptHash) + }) + + it('adds eventListener that callback throws an error', async () => { + const eventName = 'Transfer' + + const eventPromise = new Promise((resolve, reject) => { + const callBack = () => { + reject('Error') + eventListener.removeAllEventListenersOfEvent(gasScriptHash, eventName) + throw 'Error' + } + + eventListener.addEventListener(gasScriptHash, eventName, callBack) + }) + + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await assert.rejects(async () => eventPromise) + }) + + it('removes an eventListener', async () => { + const eventName = 'Transfer' + let called = 0 + + const callBack: Neo3EventListenerCallback = (notification: Neo3EventWithState) => { + assert(notification) + called += 1 + } + + eventListener.addEventListener(gasScriptHash, eventName, callBack) + + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + + let txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await wait(waitTime) + assert(called === 1, 'Callback should be called once') + + eventListener.removeEventListener(gasScriptHash, eventName, callBack) + txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await wait(waitTime) + assert(called === 1, "Callback isn't called after removeEventListener") + }) + + it('removes all eventListeners of a contract', async () => { + const eventName = 'Transfer' + let called = 0 + + const callBack: Neo3EventListenerCallback = (notification: Neo3EventWithState) => { + assert(notification) + called += 1 + } + + eventListener.addEventListener(gasScriptHash, eventName, callBack) + + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + + let txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await wait(waitTime) + assert(called === 1, 'Callback should be called once') + + eventListener.removeAllEventListenersOfContract(gasScriptHash) + txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await wait(waitTime) + assert(called === 1, "Callback isn't called after removeEventListener") + }) + + it('removes all eventListeners of an event', async () => { + const eventName = 'Transfer' + let called = 0 + + const callBack: Neo3EventListenerCallback = (notification: Neo3EventWithState) => { + assert(notification) + called += 1 + } + + eventListener.addEventListener(gasScriptHash, eventName, callBack) + + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + + let txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await wait(waitTime) + assert(called === 1, 'Callback should be called once') + + eventListener.removeAllEventListenersOfEvent(gasScriptHash, eventName) + txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + assert(txId, 'Transaction ID should be returned') + + await wait(waitTime) + assert(called === 1, "Callback isn't called after removeEventListener") + }) + + it('waits for the application log', async () => { + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + + const applicationLog = await eventListener.waitForApplicationLog(txId) + + assert(applicationLog.txid === txId, 'Transaction ID should be the same') + assert(applicationLog.executions.length === 1, 'There should be one execution') + assert(applicationLog.executions[0].trigger === 'Application', 'Trigger should be Application') + assert(applicationLog.executions[0].vmstate === 'HALT', 'VMState should be HALT') + assert(applicationLog.executions[0].gasconsumed !== undefined, 'Gas consumed should be returned') + assert(applicationLog.executions[0].stack.length === 1, 'Stack should be returned') + assert(applicationLog.executions[0].stack[0].type === 'Boolean', 'Stack type should be a boolean') + assert(applicationLog.executions[0].stack[0].value === true, 'Stack value should be true') + assert(applicationLog.executions[0].notifications.length === 1, 'Notification should be returned') + assert( + applicationLog.executions[0].notifications[0].contract === gasScriptHash, + 'Notification should be sent by GasToken', + ) + assert(applicationLog.executions[0].notifications[0].eventname === 'Transfer', 'Notification should be Transfer') + assert( + applicationLog.executions[0].notifications[0].state.type === 'Array', + 'Notification state should be an array', + ) + assert(applicationLog.executions[0].notifications[0].state.value, 'Transfer notification should be emitted') + }) + + it('exceeds the time to await for the application log', async () => { + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + + const fastEventListener = new NeonEventListener(rpcAddress, { + waitForApplicationLog: { maxAttempts: 1, waitMs: 10 }, + }) + + await assert.rejects(fastEventListener.waitForApplicationLog(txId)) + }) + + it('confirms Halt', async () => { + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + const applicationLog = await eventListener.waitForApplicationLog(txId) + + eventListener.confirmHalt(applicationLog) + }) + + it('confirms Halt on a fault state', async () => { + const applicationLog: Neo3ApplicationLog = { + txid: '', + executions: [ + { + trigger: 'Application', + vmstate: 'FAULT', + gasconsumed: '0', + notifications: [], + }, + ], + } + + assert.throws(() => eventListener.confirmHalt(applicationLog)) + }) + + it('confirms stack true', async () => { + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + const applicationLog = await eventListener.waitForApplicationLog(txId) + + eventListener.confirmStackTrue(applicationLog) + + const txIdFalse = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '-100')) + const applicationLogFalse = await eventListener.waitForApplicationLog(txIdFalse) + + await assert.rejects(async () => { + eventListener.confirmStackTrue(applicationLogFalse) + }) + }) + + it('confirms stack true on an empty stack', async () => { + const applicationLog: Neo3ApplicationLog = { + txid: '', + executions: [], + } + + assert.throws(() => eventListener.confirmStackTrue(applicationLog)) + }) + + it('gets the notification state', async () => { + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + const applicationLog = await eventListener.waitForApplicationLog(txId) + + const notificationState = eventListener.getNotificationState(applicationLog, { + contract: gasScriptHash, + eventname: 'Transfer', + }) + + assert(notificationState !== undefined, 'Notification state should be returned') + assert(TypeChecker.isStackTypeArray(notificationState.state), 'Notification state should be an array') + + const senderStack = notificationState.state.value[0] + assert(TypeChecker.isStackTypeByteString(senderStack), 'Sender should be a byte string') + assert( + NeonParser.reverseHex(NeonParser.base64ToHex(senderStack.value)) === sender.scriptHash, + 'Sender should be the first element', + ) + + const receiverStack = notificationState.state.value[1] + assert(TypeChecker.isStackTypeByteString(receiverStack), 'Receiver should be a byte string') + assert( + NeonParser.reverseHex(NeonParser.base64ToHex(receiverStack.value)) === receiver.scriptHash, + 'Receiver should be the second element', + ) + + const amountStack = notificationState.state.value[2] + assert(TypeChecker.isStackTypeInteger(amountStack), 'Amount should be an integer') + assert(amountStack.value === '100', 'Amount should be the third element') + }) + + it('confirms a transaction', async () => { + const sender = accountWithGas + const receiver = new wallet.Account() + + const neoInvoker = await NeonInvoker.init({ rpcAddress, account: sender }) + const txId = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '100')) + const applicationLog = await eventListener.waitForApplicationLog(txId) + + eventListener.confirmTransaction(applicationLog) + eventListener.confirmTransaction(applicationLog, { contract: gasScriptHash, eventname: 'Transfer' }) + eventListener.confirmTransaction(applicationLog, { contract: gasScriptHash, eventname: 'Transfer' }, true) + + const txIdFalse = await neoInvoker.invokeFunction(transferInvocation(sender, receiver, '-100')) + const applicationLogFalse = await eventListener.waitForApplicationLog(txIdFalse) + + eventListener.confirmTransaction(applicationLogFalse) + await assert.rejects(async () => { + eventListener.confirmTransaction(applicationLogFalse, { contract: gasScriptHash, eventname: 'Transfer' }) + }) + await assert.rejects(async () => { + eventListener.confirmTransaction(applicationLogFalse, { contract: gasScriptHash, eventname: 'Transfer' }, true) + }) + }) }) diff --git a/packages/neon-dappkit/test/setup-neo-go.ts b/packages/neon-dappkit/test/setup-neo-go.ts index 72f1aad..aeec3f5 100644 --- a/packages/neon-dappkit/test/setup-neo-go.ts +++ b/packages/neon-dappkit/test/setup-neo-go.ts @@ -1,90 +1,90 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { https } from 'follow-redirects'; -import { execSync } from "child_process"; +import * as fs from 'fs' +import * as path from 'path' +import { https } from 'follow-redirects' +import { execSync } from 'child_process' async function installNeoGo(): Promise { - const toolsDir = path.resolve(path.join(__dirname, '..', 'neogo')); - - const platform = process.platform; - let osType = platform.toString(); - let fileExtension = ''; - if (platform == "win32") { - osType = "windows"; - fileExtension = '.exe'; - } - - const goCompilerExecutablePath = path.resolve(path.join(toolsDir, `neogo${fileExtension}`)) - if (fs.existsSync(goCompilerExecutablePath)) { - return goCompilerExecutablePath; - } - - const version = '0.105.1'; - const arch = process.arch; - - let archType = 'arm64'; - if (arch == 'x64') { - archType = 'amd64'; - } - - if (osType == "windows" && archType == "arm64") { - throw new Error(`Unsupported architecture: ${osType}-${arch}`); - } - - - if (!fs.existsSync(toolsDir)) { - fs.mkdirSync(toolsDir, { recursive: true }) - } - - if (!fs.existsSync(goCompilerExecutablePath)) { - if (osType == "darwin" && archType == "arm64") { - const neoGoArchivePage = "https://github.com/nspcc-dev/neo-go/archive/refs/tags"; - const downloadUrl = `${neoGoArchivePage}/v${version}.zip`; - const zipPath = path.join(toolsDir, 'neogo.zip'); - - await downloadAndVerify(downloadUrl, zipPath); - - const AdmZip = require('adm-zip'); - const zip = new AdmZip(zipPath); - - zip.extractAllTo(toolsDir, true); - const extractedFolderPath = path.join(toolsDir, "neo-go-" + version); - console.log(extractedFolderPath); - execSync(`make -C ${extractedFolderPath}`); - - } else { - const fileName = `neo-go-${osType}-${archType}${fileExtension}`; - const neoGoReleasePage = "https://github.com/nspcc-dev/neo-go/releases"; - const downloadUrl = `${neoGoReleasePage}/download/v${version}/${fileName}`; - - await downloadAndVerify(downloadUrl, goCompilerExecutablePath); - } + const toolsDir = path.resolve(path.join(__dirname, '..', 'neogo')) + + const platform = process.platform + let osType = platform.toString() + let fileExtension = '' + if (platform == 'win32') { + osType = 'windows' + fileExtension = '.exe' + } + + const goCompilerExecutablePath = path.resolve(path.join(toolsDir, `neogo${fileExtension}`)) + if (fs.existsSync(goCompilerExecutablePath)) { + return goCompilerExecutablePath + } + + const version = '0.105.1' + const arch = process.arch + + let archType = 'arm64' + if (arch == 'x64') { + archType = 'amd64' + } + + if (osType == 'windows' && archType == 'arm64') { + throw new Error(`Unsupported architecture: ${osType}-${arch}`) + } + + if (!fs.existsSync(toolsDir)) { + fs.mkdirSync(toolsDir, { recursive: true }) + } + + if (!fs.existsSync(goCompilerExecutablePath)) { + if (osType == 'darwin' && archType == 'arm64') { + const neoGoArchivePage = 'https://github.com/nspcc-dev/neo-go/archive/refs/tags' + const downloadUrl = `${neoGoArchivePage}/v${version}.zip` + const zipPath = path.join(toolsDir, 'neogo.zip') + + await downloadAndVerify(downloadUrl, zipPath) + + /* eslint-disable @typescript-eslint/no-var-requires */ + const AdmZip = require('adm-zip') + const zip = new AdmZip(zipPath) + + zip.extractAllTo(toolsDir, true) + const extractedFolderPath = path.join(toolsDir, 'neo-go-' + version) + console.log(extractedFolderPath) + execSync(`make -C ${extractedFolderPath}`) + } else { + const fileName = `neo-go-${osType}-${archType}${fileExtension}` + const neoGoReleasePage = 'https://github.com/nspcc-dev/neo-go/releases' + const downloadUrl = `${neoGoReleasePage}/download/v${version}/${fileName}` + + await downloadAndVerify(downloadUrl, goCompilerExecutablePath) } + } - fs.chmodSync(goCompilerExecutablePath, '755') - execSync(`${goCompilerExecutablePath} node -h`); - return goCompilerExecutablePath; + fs.chmodSync(goCompilerExecutablePath, '755') + execSync(`${goCompilerExecutablePath} node -h`) + return goCompilerExecutablePath } - async function downloadAndVerify(downloadUrl: string, downloadPath: string) { - try { - await new Promise((resolve, reject) => { - const file = fs.createWriteStream(downloadPath); - https.get(downloadUrl, (response: { pipe: (stream: fs.WriteStream) => void; }) => { - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }).on('error', (err: { message: any; }) => { - fs.unlink(downloadPath, () => { }); // Delete the file async on error - reject(err.message); - }); - }); - } catch (error) { - console.error('Error:', error); - } + try { + await new Promise((resolve, reject) => { + const file = fs.createWriteStream(downloadPath) + https + .get(downloadUrl, (response: { pipe: (stream: fs.WriteStream) => void }) => { + response.pipe(file) + file.on('finish', () => { + file.close() + resolve() + }) + }) + .on('error', (err: { message: any }) => { + fs.unlink(downloadPath, () => {}) // Delete the file async on error + reject(err.message) + }) + }) + } catch (error) { + console.error('Error:', error) + } } -installNeoGo(); +installNeoGo()