diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39e4b0ac97..ae6d584a94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -418,6 +418,9 @@ importers: '@roochnetwork/build-scripts': specifier: workspace:* version: link:../build-scripts + '@roochnetwork/test-suite': + specifier: workspace:* + version: link:../test-suite '@types/bs58check': specifier: 2.1.2 version: 2.1.2 @@ -589,6 +592,33 @@ importers: specifier: ^4.4.4 version: 4.5.3(@types/node@20.14.14)(terser@5.31.1) + sdk/typescript/test-suite: + devDependencies: + '@roochnetwork/build-scripts': + specifier: workspace:* + version: link:../build-scripts + '@types/node': + specifier: ^20.14.10 + version: 20.14.14 + '@types/tmp': + specifier: ^0.2.1 + version: 0.2.6 + testcontainers: + specifier: 10.11.0 + version: 10.11.0 + tmp: + specifier: ^0.2.1 + version: 0.2.3 + typescript: + specifier: ^5.3.3 + version: 5.4.5 + vite: + specifier: ^4.4.4 + version: 4.5.3(@types/node@20.14.14)(terser@5.31.1) + vitest: + specifier: ^1.6.0 + version: 1.6.0(@types/node@20.14.14)(happy-dom@14.12.0)(jsdom@23.2.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) + packages: '@adobe/css-tools@4.4.0': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fdbf2e5ec0..dbdb9b616d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,5 @@ packages: - 'sdk/typescript/rooch-sdk-kit' - 'sdk/typescript/templates/**' - 'sdk/typescript/rooch-create' + - 'sdk/typescript/test-suite' - 'docs/**' diff --git a/sdk/typescript/rooch-sdk/package.json b/sdk/typescript/rooch-sdk/package.json index 84da2c39b1..f872e6e931 100644 --- a/sdk/typescript/rooch-sdk/package.json +++ b/sdk/typescript/rooch-sdk/package.json @@ -47,6 +47,7 @@ "keywords": ["Rooch", "Rooch Network", "Move"], "devDependencies": { "@roochnetwork/build-scripts": "workspace:*", + "@roochnetwork/test-suite": "workspace:*", "@iarna/toml": "^2.2.5", "@types/node": "^20.14.10", "@types/tmp": "^0.2.1", diff --git a/sdk/typescript/rooch-sdk/test-e2e/setup.ts b/sdk/typescript/rooch-sdk/test-e2e/setup.ts index 838745c975..51f00854ba 100644 --- a/sdk/typescript/rooch-sdk/test-e2e/setup.ts +++ b/sdk/typescript/rooch-sdk/test-e2e/setup.ts @@ -1,40 +1,25 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -import tmp, { DirResult } from 'tmp' -import { execSync } from 'child_process' -import { Network, StartedNetwork } from 'testcontainers' - +import * as fs from 'fs' +import tmp from 'tmp' import { RoochAddress } from '../src/address/index.js' import { getRoochNodeUrl, RoochClient } from '../src/client/index.js' import { Secp256k1Keypair } from '../src/keypairs/index.js' import { Transaction } from '../src/transactions/index.js' import { Args } from '../src/bcs/args.js' -import * as fs from 'fs' -import { BitcoinContainer, StartedBitcoinContainer } from './env/bitcoin-container.js' -import { RoochContainer, StartedRoochContainer } from './env/rooch-container.js' -// import os from 'node:os' -import { OrdContainer, StartedOrdContainer } from './env/ord-container.js' -const bitcoinNetworkAlias = 'bitcoind' -export const DEFAULT_NODE_URL = import.meta.env.VITE_FULLNODE_URL ?? getRoochNodeUrl('localnet') +import { TestBox as TestBoxA, RoochContainer } from '@roochnetwork/test-suite' -let _defaultCmdAddress = '' +export const DEFAULT_NODE_URL = import.meta.env.VITE_FULLNODE_URL ?? getRoochNodeUrl('localnet') -export class TestBox { +export class TestBox extends TestBoxA { private client?: RoochClient keypair: Secp256k1Keypair - network?: StartedNetwork - - tmpDir: DirResult - ordContainer?: StartedOrdContainer - bitcoinContainer?: StartedBitcoinContainer - roochContainer?: StartedRoochContainer | number constructor(keypair: Secp256k1Keypair) { + super() this.keypair = keypair - tmp.setGracefulCleanup() - this.tmpDir = tmp.dirSync({ unsafeCleanup: true }) } static setup(): TestBox { @@ -42,6 +27,18 @@ export class TestBox { return new TestBox(kp) } + async loadRoochEnv( + target: RoochContainer | 'local' | 'container' = 'local', + port: number = 6768, + ): Promise { + await super.loadRoochEnv(target, port) + + this.client = new RoochClient({ + url: `http://127.0.0.1:${port}`, + }) + return + } + getClient(url = DEFAULT_NODE_URL): RoochClient { if (url === DEFAULT_NODE_URL) { if (!this.client) { @@ -57,121 +54,10 @@ export class TestBox { }) } - async loadBitcoinEnv(customContainer?: BitcoinContainer) { - if (customContainer) { - this.bitcoinContainer = await customContainer.start() - return - } - - this.bitcoinContainer = await new BitcoinContainer() - .withHostDataPath(this.tmpDir.name) - .withNetwork(await this.getNetwork()) - .withNetworkAliases(bitcoinNetworkAlias) - .start() - } - - async loadRoochEnv(customContainer?: RoochContainer) { - if (customContainer) { - await customContainer.start() - return - } - - // The container test in the linux environment is incomplete, so use it first - // if (os.platform() === 'darwin') { - const port = '6768' - - const cmds = ['server', 'start', '-n', 'local', '-d', 'TMP', '--port', port] - - if (this.bitcoinContainer) { - cmds.push( - ...[ - '--btc-rpc-url', - 'http://127.0.0.1:18443', - '--btc-rpc-username', - this.bitcoinContainer.getRpcUser(), - '--btc-rpc-password', - this.bitcoinContainer.getRpcPass(), - ], - ) - } - - cmds.push(`> ${this.tmpDir.name}/rooch.log 2>&1 & echo $!`) - - const result = this.roochCommand(cmds) - this.roochContainer = parseInt(result.toString().trim(), 10) - - await this.delay(10) - - this.client = new RoochClient({ url: `http://127.0.0.1:${port}` }) - - // return - // } - - // const container = new RoochContainer().withHostConfigPath(`${this.tmpDir.name}/.rooch`) - // await container.initializeRooch() - // - // container - // .withNetwork(await this.getNetwork()) - // .withNetworkName('local') - // .withDataDir('TMP') - // .withPort(6768) - // - // if (this.bitcoinContainer) { - // container - // .withBtcRpcUrl(`http://${bitcoinNetworkAlias}:18443`) - // .withBtcRpcUsername(this.bitcoinContainer.getRpcUser()) - // .withBtcRpcPassword(this.bitcoinContainer.getRpcPass()) - // .withBtcSyncBlockInterval(1) // Set sync interval to 1 second - // } - // - // this.roochContainer = await container.start() - // - // this.client = new RoochClient({ url: this.roochContainer.getConnectionAddress() }) - } - - async loadORDEnv(customContainer?: OrdContainer) { - if (customContainer) { - this.ordContainer = await customContainer.start() - return - } - - if (!this.bitcoinContainer) { - console.log('bitcoin container not init') - return - } - - this.ordContainer = await new OrdContainer() - .withHostDataPath(this.tmpDir.name) - .withBitcoinDataPath(this.bitcoinContainer.getHostDataPath()) - .withNetwork(await this.getNetwork()) - .withNetworkAliases('ord') - .withBtcRpcUrl(`http://${bitcoinNetworkAlias}:18443`) - .withBtcRpcUsername(this.bitcoinContainer.getRpcUser()) - .withBtcRpcPassword(this.bitcoinContainer.getRpcPass()) - .start() - } - - unloadContainer() { - this.bitcoinContainer?.stop() - this.ordContainer?.stop() - - if (typeof this.roochContainer === 'number') { - process.kill(this.roochContainer) - } else { - this.roochContainer?.stop() - } - - this.tmpDir.removeCallback() - } - address(): RoochAddress { return this.keypair.getRoochAddress() } - delay(second: number) { - return new Promise((resolve) => setTimeout(resolve, second * 1000)) - } - async signAndExecuteTransaction(tx: Transaction) { const result = await this.getClient().signAndExecuteTransaction({ transaction: tx, @@ -181,25 +67,6 @@ export class TestBox { return result.execution_info.status.type === 'executed' } - private async getNetwork() { - if (!this.network) { - this.network = await new Network().start() - } - return this.network - } - - shell(args: string[] | string): string { - return execSync(`${typeof args === 'string' ? args : args.join(' ')}`, { - encoding: 'utf-8', - }) - } - - roochCommand(args: string[] | string): string { - return execSync(`cargo run --bin rooch ${typeof args === 'string' ? args : args.join(' ')}`, { - encoding: 'utf-8', - }) - } - async publishPackage( packagePath: string, box: TestBox, @@ -235,52 +102,4 @@ export class TestBox { return false } } - - async cmdPublishPackage( - packagePath: string, - options: { - namedAddresses: string - } = { - namedAddresses: 'rooch_examples=default', - }, - ) { - const result = this.roochCommand( - `move publish -p ${packagePath} --named-addresses ${options.namedAddresses} --json`, - ) - const { execution_info } = JSON.parse(result) - - return execution_info?.status?.type === 'executed' - } - - /** - * Retrieves the default account address. - * - * This method lists all accounts and returns the address of the first active account found. - * If no active account is present, it throws an error. - * - * @returns {Promise} A promise that resolves with the address of the default account. - * @throws {Error} When no active account address is found. - */ - async defaultCmdAddress(): Promise { - if (!_defaultCmdAddress) { - const accounts = JSON.parse(this.roochCommand(['account', 'list', '--json'])) - - if (Array.isArray(accounts)) { - for (const account of accounts) { - if (account.active) { - _defaultCmdAddress = account.local_account.hex_address - } - } - } else { - const defaultAddr = accounts['default'] - _defaultCmdAddress = defaultAddr.hex_address - } - - if (!_defaultCmdAddress) { - throw new Error('No active account address') - } - } - - return _defaultCmdAddress - } } diff --git a/sdk/typescript/test-suite/.gitignore b/sdk/typescript/test-suite/.gitignore new file mode 100644 index 0000000000..668a8b567a --- /dev/null +++ b/sdk/typescript/test-suite/.gitignore @@ -0,0 +1,23 @@ +.env +.DS_Store +*/**/.DS_Store +npm-debug.log +.npm/ +/coverage +/tmp +node_modules +.idea/ +.history/ +.vscode/ +dist/ +.nyc_output/ +build/ + +# Doc generation output +docs/ + +# client generation output +generated/ + +# yarn error +yarn-error.log diff --git a/sdk/typescript/test-suite/LICENSE b/sdk/typescript/test-suite/LICENSE new file mode 100644 index 0000000000..66c0c47fd1 --- /dev/null +++ b/sdk/typescript/test-suite/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Root Branch Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sdk/typescript/test-suite/package.json b/sdk/typescript/test-suite/package.json new file mode 100644 index 0000000000..1ed593f2ff --- /dev/null +++ b/sdk/typescript/test-suite/package.json @@ -0,0 +1,62 @@ +{ + "name": "@roochnetwork/test-suite", + "author": "Rooch.network ", + "version": "0.1.1", + "description": "Rooch Test Suite", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" +}, + "packageManager": "pnpm@8.6.6", + "scripts": { + "clean": "rimraf tsconfig.tsbuildinfo rimraf dist", + "build": "pnpm build:package", + "build:package": "build-package", + "vitest": "vitest", + "test": "pnpm test:unit && pnpm test:e2e", + "test:unit": "vitest run src", + "test:e2e": "pnpm prepare:e2e && wait-on tcp:0.0.0.0:6767 -l --timeout 180000 && vitest run e2e; pnpm stop:e2e", + "test:e2e:nowait": "vitest run e2e", + "prepare:e2e": "nohup cargo run --bin rooch server start -n local -d TMP --port 6767 > /dev/null 2>&1 &", + "stop:e2e": "lsof -ti:6767 | xargs kill", + "prepublishOnly": "pnpm build", + "size": "size-limit", + "analyze": "size-limit --why", + "eslint:check": "eslint --max-warnings=0 .", + "eslint:fix": "pnpm eslint:check --fix", + "prettier:check": "prettier -c --ignore-unknown .", + "prettier:fix": "prettier -w --ignore-unknown .", + "lint": "pnpm eslint:check && pnpm prettier:check", + "lint:fix": "pnpm eslint:fix && pnpm prettier:fix" +}, + "repository": { + "type": "git", + "url": "https://github.com/rooch-network/rooch.git" +}, + "homepage": "https://github.com/rooch-network/rooch", + "bugs": { + "url": "https://github.com/rooch-network/rooch/issues" +}, + "files": ["dist", "src"], + "type": "module", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/cjs/index.d.ts", + "devDependencies": { + "@roochnetwork/build-scripts": "workspace:*", + "@types/node": "^20.14.10", + "@types/tmp": "^0.2.1", + "typescript": "^5.3.3", + "vite": "^4.4.4", + "vitest": "^1.6.0", + "tmp": "^0.2.1", + "testcontainers": "10.11.0" +}, + "dependencies": { + +}, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" +} +} diff --git a/sdk/typescript/rooch-sdk/test-e2e/env/bitcoin-container.ts b/sdk/typescript/test-suite/src/container/bitcoin.ts similarity index 100% rename from sdk/typescript/rooch-sdk/test-e2e/env/bitcoin-container.ts rename to sdk/typescript/test-suite/src/container/bitcoin.ts index d6cad39b00..f09286ef0c 100644 --- a/sdk/typescript/rooch-sdk/test-e2e/env/bitcoin-container.ts +++ b/sdk/typescript/test-suite/src/container/bitcoin.ts @@ -1,6 +1,8 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 +import fs from 'fs' +import path from 'node:path' import * as crypto from 'crypto' import { AbstractStartedContainer, @@ -9,8 +11,6 @@ import { StartedTestContainer, Wait, } from 'testcontainers' -import path from 'node:path' -import fs from 'fs' const BITCOIN_PORTS = [18443, 18444, 28333, 28332] diff --git a/sdk/typescript/rooch-sdk/test-e2e/env/ord-container.ts b/sdk/typescript/test-suite/src/container/ord.ts similarity index 99% rename from sdk/typescript/rooch-sdk/test-e2e/env/ord-container.ts rename to sdk/typescript/test-suite/src/container/ord.ts index fd583cf41e..3736301454 100644 --- a/sdk/typescript/rooch-sdk/test-e2e/env/ord-container.ts +++ b/sdk/typescript/test-suite/src/container/ord.ts @@ -1,9 +1,10 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -import { AbstractStartedContainer, GenericContainer, StartedTestContainer } from 'testcontainers' -import path from 'node:path' import fs from 'fs' +import path from 'node:path' + +import { AbstractStartedContainer, GenericContainer, StartedTestContainer } from 'testcontainers' export class OrdContainer extends GenericContainer { private btcRpcUrl = 'http:://bitcoind:18443' @@ -21,6 +22,7 @@ export class OrdContainer extends GenericContainer { this.bitcoinDataPath = bitcoinHostPath return this } + public withHostDataPath(hostPath: string): this { this.hostDataPath = path.join(hostPath, 'ord') fs.mkdirSync(this.hostDataPath, { recursive: true }) diff --git a/sdk/typescript/rooch-sdk/test-e2e/env/rooch-container.ts b/sdk/typescript/test-suite/src/container/rooch.ts similarity index 100% rename from sdk/typescript/rooch-sdk/test-e2e/env/rooch-container.ts rename to sdk/typescript/test-suite/src/container/rooch.ts diff --git a/sdk/typescript/test-suite/src/index.ts b/sdk/typescript/test-suite/src/index.ts new file mode 100644 index 0000000000..eb460f17f7 --- /dev/null +++ b/sdk/typescript/test-suite/src/index.ts @@ -0,0 +1,7 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +export * from './testbox.js' +export * from './container/ord.js' +export * from './container/bitcoin.js' +export * from './container/rooch.js' diff --git a/sdk/typescript/test-suite/src/testbox.ts b/sdk/typescript/test-suite/src/testbox.ts new file mode 100644 index 0000000000..73486b02e6 --- /dev/null +++ b/sdk/typescript/test-suite/src/testbox.ts @@ -0,0 +1,205 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import tmp, { DirResult } from 'tmp' +import { execSync } from 'child_process' +import { Network, StartedNetwork } from 'testcontainers' + +import { OrdContainer, StartedOrdContainer } from './container/ord.js' +import { RoochContainer, StartedRoochContainer } from './container/rooch.js' +import { BitcoinContainer, StartedBitcoinContainer } from './container/bitcoin.js' + +const ordNetworkAlias = 'ord' +const bitcoinNetworkAlias = 'bitcoind' +let _defaultCmdAddress = '' + +export class TestBox { + tmpDir: DirResult + network?: StartedNetwork + ordContainer?: StartedOrdContainer + bitcoinContainer?: StartedBitcoinContainer + roochContainer?: StartedRoochContainer | number + + constructor() { + tmp.setGracefulCleanup() + this.tmpDir = tmp.dirSync({ unsafeCleanup: true }) + } + + async loadBitcoinEnv(customContainer?: BitcoinContainer) { + if (customContainer) { + this.bitcoinContainer = await customContainer.start() + return + } + + this.bitcoinContainer = await new BitcoinContainer() + .withHostDataPath(this.tmpDir.name) + .withNetwork(await this.getNetwork()) + .withNetworkAliases(bitcoinNetworkAlias) + .start() + + await this.delay(5) + } + + async loadORDEnv(customContainer?: OrdContainer) { + if (customContainer) { + this.ordContainer = await customContainer.start() + return + } + + if (!this.bitcoinContainer) { + console.log('bitcoin container not init') + return + } + + this.ordContainer = await new OrdContainer() + .withHostDataPath(this.tmpDir.name) + .withBitcoinDataPath(this.bitcoinContainer.getHostDataPath()) + .withNetwork(await this.getNetwork()) + .withNetworkAliases(ordNetworkAlias) + .withBtcRpcUrl(`http://${bitcoinNetworkAlias}:18443`) + .withBtcRpcUsername(this.bitcoinContainer.getRpcUser()) + .withBtcRpcPassword(this.bitcoinContainer.getRpcPass()) + .start() + + await this.delay(5) + } + + async loadRoochEnv( + target: RoochContainer | 'local' | 'container' = 'local', + port: number = 6767, + ) { + if (target && typeof target !== 'string') { + await target.start() + return + } + + // The container test in the linux environment is incomplete, so use it first + if (target === 'local') { + const cmds = ['server', 'start', '-n', 'local', '-d', 'TMP', '--port', port.toString()] + + if (this.bitcoinContainer) { + cmds.push( + ...[ + '--btc-rpc-url', + 'http://127.0.0.1:18443', + '--btc-rpc-username', + this.bitcoinContainer.getRpcUser(), + '--btc-rpc-password', + this.bitcoinContainer.getRpcPass(), + ], + ) + } + + cmds.push(`> ${this.tmpDir.name}/rooch.log 2>&1 & echo $!`) + + const result = this.roochCommand(cmds) + this.roochContainer = parseInt(result.toString().trim(), 10) + + await this.delay(5) + return + } + + const container = new RoochContainer().withHostConfigPath(`${this.tmpDir.name}/.rooch`) + await container.initializeRooch() + + container + .withNetwork(await this.getNetwork()) + .withNetworkName('local') + .withDataDir('TMP') + .withPort(port) + + if (this.bitcoinContainer) { + container + .withBtcRpcUrl(`http://${bitcoinNetworkAlias}:18443`) + .withBtcRpcUsername(this.bitcoinContainer.getRpcUser()) + .withBtcRpcPassword(this.bitcoinContainer.getRpcPass()) + .withBtcSyncBlockInterval(1) // Set sync interval to 1 second + } + + this.roochContainer = await container.start() + } + + unloadContainer() { + this.bitcoinContainer?.stop() + this.ordContainer?.stop() + + if (typeof this.roochContainer === 'number') { + process.kill(this.roochContainer) + } else { + this.roochContainer?.stop() + } + + this.tmpDir.removeCallback() + } + + delay(second: number) { + return new Promise((resolve) => setTimeout(resolve, second * 1000)) + } + + shell(args: string[] | string): string { + return execSync(`${typeof args === 'string' ? args : args.join(' ')}`, { + encoding: 'utf-8', + }) + } + + roochCommand(args: string[] | string): string { + return execSync(`cargo run --bin rooch ${typeof args === 'string' ? args : args.join(' ')}`, { + encoding: 'utf-8', + }) + } + + async cmdPublishPackage( + packagePath: string, + options: { + namedAddresses: string + } = { + namedAddresses: 'rooch_examples=default', + }, + ) { + const result = this.roochCommand( + `move publish -p ${packagePath} --named-addresses ${options.namedAddresses} --json`, + ) + const { execution_info } = JSON.parse(result) + + return execution_info?.status?.type === 'executed' + } + + /** + * Retrieves the default account address. + * + * This method lists all accounts and returns the address of the first active account found. + * If no active account is present, it throws an error. + * + * @returns {Promise} A promise that resolves with the address of the default account. + * @throws {Error} When no active account address is found. + */ + async defaultCmdAddress(): Promise { + if (!_defaultCmdAddress) { + const accounts = JSON.parse(this.roochCommand(['account', 'list', '--json'])) + + if (Array.isArray(accounts)) { + for (const account of accounts) { + if (account.active) { + _defaultCmdAddress = account.local_account.hex_address + } + } + } else { + const defaultAddr = accounts['default'] + _defaultCmdAddress = defaultAddr.hex_address + } + + if (!_defaultCmdAddress) { + throw new Error('No active account address') + } + } + + return _defaultCmdAddress + } + + private async getNetwork() { + if (!this.network) { + this.network = await new Network().start() + } + return this.network + } +} diff --git a/sdk/typescript/test-suite/tsconfig.esm.json b/sdk/typescript/test-suite/tsconfig.esm.json new file mode 100644 index 0000000000..aa4b6cbb13 --- /dev/null +++ b/sdk/typescript/test-suite/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "dist/esm" + } +} diff --git a/sdk/typescript/test-suite/tsconfig.json b/sdk/typescript/test-suite/tsconfig.json new file mode 100644 index 0000000000..838ffc1494 --- /dev/null +++ b/sdk/typescript/test-suite/tsconfig.json @@ -0,0 +1,13 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +{ + "extends": "../build-scripts/tsconfig.shared.json", + "include": ["src"], + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist/cjs", + "rootDir": "src", + "isolatedModules": true + } +} diff --git a/sdk/typescript/test-suite/vite.config.ts b/sdk/typescript/test-suite/vite.config.ts new file mode 100644 index 0000000000..c5f5971223 --- /dev/null +++ b/sdk/typescript/test-suite/vite.config.ts @@ -0,0 +1,22 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import * as path from 'path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + test: { + minThreads: 1, + maxThreads: 8, + hookTimeout: 1000000, + testTimeout: 1000000, + env: { + NODE_ENV: 'test', + }, + }, +})