Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
"main": "cjs/index.js",
"types": "cjs/index.d.ts",
"scripts": {
"testCommand": "jest",
"prepublishOnly": "npm run build",
"build": "tsc -p ./tsconfig-cjs.json",
"test": "node-state -e -n -m typescript -i ghcr.io/wavesplatform/waves-private-node:finality -o ./test/_state.ts -r"
"test": "jest"
},
"repository": {
"type": "git",
Expand All @@ -24,6 +23,8 @@
"cjs"
],
"jest": {
"globalSetup": "<rootDir>/test/globalSetupWrapper.js",
"globalTeardown": "<rootDir>/test/globalTeardownWrapper.js",
"moduleFileExtensions": [
"ts",
"tsx",
Expand All @@ -42,11 +43,13 @@
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@types/dockerode": "^3.3.0",
"@types/jest": "^30.0.0",
"@typescript-eslint/eslint-plugin": "^8.56.0",
"@typescript-eslint/parser": "^8.56.0",
"@waves/node-state": "^0.2.0-snapshot.2",
"@waves/waves-transactions": "4.4.0-snapshot.3",
"@waves/waves-transactions": "4.4.0-snapshot.4",
"dockerode": "^5.0.0",
"ts-node": "^10.9.2",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
Expand Down
1 change: 0 additions & 1 deletion test/api-node/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {create} from '../../src';
import {isStringOrNumber} from '../extendedMatcher'
import {ACCOUNT_SCRIPT, CHAIN_ID, DAP_SCRIPT, MASTER_ACCOUNT, NODE_URL, STATE} from "../_state";
import { issue } from '@waves/waves-transactions';
import {MASTER_ACCOUNT_SEED} from "@waves/node-state/dist/constants";

const api: ReturnType<typeof create> = create(NODE_URL);

Expand Down
85 changes: 85 additions & 0 deletions test/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Dockerode from 'dockerode';
import { writeFile } from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';
import os from 'os';
import { writeState } from './setup/write';

const NODE_IMAGE = 'wavesplatform/waves-private-node';
const HOST_PORT = 6869;
const CONTAINER_PORT = 6869;

const CONTAINER_ID_FILE = path.join(os.tmpdir(), 'node-api-js-test-container-id');

export default async function globalSetup(): Promise<void> {
let nodeUrl: string;

if (process.env.NODE_URL) {
nodeUrl = process.env.NODE_URL;
console.log(`Using existing node at ${nodeUrl}`);
} else {
nodeUrl = await startContainer();
await waitForNode(nodeUrl);
}

await writeState(nodeUrl, path.join(__dirname, '_state.ts'));
}

function isInsideDocker(): boolean {
return existsSync('/.dockerenv');
}

async function startContainer(): Promise<string> {
const docker = new Dockerode();

await pullImage(docker, NODE_IMAGE);

const container = await docker.createContainer({
Image: NODE_IMAGE,
HostConfig: {
PortBindings: {
[`${CONTAINER_PORT}/tcp`]: [{ HostPort: String(HOST_PORT) }]
}
}
});

await container.start();
await writeFile(CONTAINER_ID_FILE, container.id);
console.log(`Started container ${container.id}`);

const host = isInsideDocker() ? 'host.docker.internal' : 'localhost';
return `http://${host}:${HOST_PORT}`;
}

async function pullImage(docker: Dockerode, image: string): Promise<void> {
const images = await docker.listImages({ filters: { reference: [image] } });
if (images.length > 0) return;

return new Promise((resolve, reject) => {
docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {
if (err) return reject(err);
docker.modem.followProgress(stream, (err: Error | null) => {
if (err) reject(err);
else resolve();
});
});
});
}

async function waitForNode(nodeUrl: string, timeoutMs = 120000): Promise<void> {
const deadline = Date.now() + timeoutMs;
console.log(`Waiting for node at ${nodeUrl}...`);
while (Date.now() < deadline) {
try {
const response = await fetch(`${nodeUrl}/node/status`);
if (response.ok) {
console.log('Node is ready');
return;
}
} catch {
// not ready yet
}
await new Promise(r => setTimeout(r, 1000));
}
throw new Error(`Node at ${nodeUrl} did not become ready within ${timeoutMs}ms`);
}
6 changes: 6 additions & 0 deletions test/globalSetupWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require('ts-node').register({
project: require('path').join(__dirname, '..', 'tsconfig.json'),
transpileOnly: true,
compilerOptions: { module: 'commonjs' }
});
module.exports = require('./globalSetup').default;
28 changes: 28 additions & 0 deletions test/globalTeardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Dockerode from 'dockerode';
import { readFile, unlink } from 'fs/promises';
import os from 'os';
import path from 'path';

const CONTAINER_ID_FILE = path.join(os.tmpdir(), 'node-api-js-test-container-id');

export default async function globalTeardown(): Promise<void> {
let containerId: string;
try {
containerId = (await readFile(CONTAINER_ID_FILE, 'utf8')).trim();
} catch {
return;
}

const docker = new Dockerode();
const container = docker.getContainer(containerId);

try {
await container.stop();
await container.remove();
console.log(`Stopped and removed container ${containerId}`);
} catch (e) {
console.error(`Failed to stop/remove container ${containerId}: ${e}`);
} finally {
await unlink(CONTAINER_ID_FILE).catch(() => null);
}
}
6 changes: 6 additions & 0 deletions test/globalTeardownWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require('ts-node').register({
project: require('path').join(__dirname, '..', 'tsconfig.json'),
transpileOnly: true,
compilerOptions: { module: 'commonjs' }
});
module.exports = require('./globalTeardown').default;
58 changes: 58 additions & 0 deletions test/setup/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"ASSETS": {
"BTC": {
"name": "WBTC"
},
"USD": {
"name": "WUSD"
},
"EUR": {
"name": "WEUR"
},
"ETH": {
"name": "WETH"
},
"SMART": {
"name": "Smart",
"script": true
},
"DUMB": {
"name": "Dumb",
"owner": "SIMPLE",
"sponsorship": true
}
},
"ACCOUNTS": {
"FOR_SCRIPT": {
"script": "dApp"
},
"SIMPLE": {
"alias": true,
"balance": {
"BTC": 10000000000,
"SMART": 10000000000
},
"lease": {
"amount": 1000000
},
"data": {
"key": {
"value": "123",
"type": "string"
},
"count": {
"value": 1,
"type": "integer"
}
}
},
"SMART": {
"script": true,
"alias": true,
"balance": {
"BTC": 10000000000,
"SMART": 10000000000
}
}
}
}
5 changes: 5 additions & 0 deletions test/setup/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const MASTER_ACCOUNT_SEED = 'waves private node seed with waves tokens';
export const CHAIN_ID = 'R';
export const SMART_ASSET_SCRIPT = 'base64:BAbMtW/U';
export const DAP_SCRIPT = 'base64:AAIEAAAAAAAAAAQIAhIAAAAAAAAAAAEAAAABaQEAAAAEY2FsbAAAAAAFAAAAA25pbAAAAAEAAAACdHgBAAAABnZlcmlmeQAAAAAJAAH0AAAAAwgFAAAAAnR4AAAACWJvZHlCeXRlcwkAAZEAAAACCAUAAAACdHgAAAAGcHJvb2ZzAAAAAAAAAAAACAUAAAACdHgAAAAPc2VuZGVyUHVibGljS2V5oysiIA==';
export const ACCOUNT_SCRIPT = 'base64:BAkAAfQAAAADCAUAAAACdHgAAAAJYm9keUJ5dGVzCQABkQAAAAIIBQAAAAJ0eAAAAAZwcm9vZnMAAAAAAAAAAAAIBQAAAAJ0eAAAAA9zZW5kZXJQdWJsaWNLZXnYG58I';
119 changes: 119 additions & 0 deletions test/setup/createAccounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { alias, data, lease, libs, setScript, transfer } from '@waves/waves-transactions';
import { fetchBalanceDetails } from '../../src/api-node/addresses';
import { ACCOUNT_SCRIPT, CHAIN_ID, DAP_SCRIPT, MASTER_ACCOUNT_SEED } from './constants';
import { broadcastAndWait } from './utils';

interface IAccount {
seed?: string;
script?: boolean | string;
alias?: boolean | string;
balance?: Record<string, string | number>;
data?: Record<string, { value: string | boolean | number; type: 'string' | 'boolean' | 'integer' | 'binary' }>;
lease?: { amount: string | number };
}

interface IAccountResult {
seed: string;
alias: string | undefined;
address: string;
publicKey: string;
scripted: boolean;
data: IAccount['data'];
lease: IAccount['lease'];
}

export default async function createAccounts(
nodeUrl: string,
accounts: Record<string, IAccount>
): Promise<Record<string, IAccountResult>> {
const entries = await Promise.all(
Object.entries(accounts).map(async ([key, account]) => {
const seed = account.seed || libs.crypto.randomSeed();
const address = libs.crypto.address(seed, CHAIN_ID);
const publicKey = libs.crypto.publicKey(seed);
const userAlias = account.alias
? typeof account.alias === 'string'
? account.alias
: `${key}@${Date.now()}`.toLocaleLowerCase()
: undefined;

console.log(`Add account ${key} ${address}`);

await setBalance(nodeUrl, address, 100 * Math.pow(10, 8));

if (userAlias) {
const tx = alias({
chainId: CHAIN_ID,
alias: userAlias,
additionalFee: 0.001 * Math.pow(10, 8)
}, seed);
await broadcastAndWait(nodeUrl, tx);
}

if (account.data) {
await Promise.all(
Object.entries(account.data).map(async ([key, { type, value }]) => {
const tx = data({
chainId: CHAIN_ID,
data: [{ key, type, value } as any]
}, seed);
await broadcastAndWait(nodeUrl, tx);
})
);
}

if (account.script) {
const script = typeof account.script === 'boolean'
? ACCOUNT_SCRIPT
: account.script === 'dApp'
? DAP_SCRIPT
: account.script;
await addScript(nodeUrl, seed, script);
}

if (account.lease) {
const randomAddress = libs.crypto.address(libs.crypto.randomSeed(), CHAIN_ID);
await setLeasing(nodeUrl, randomAddress, Math.pow(10, 8));
}

const { available } = await fetchBalanceDetails(nodeUrl, address);
const toSend = 100 * Math.pow(10, 8) - (+available);
await setBalance(nodeUrl, address, toSend);

return {
[key]: { seed, alias: userAlias, address, publicKey, scripted: !!account.script, data: account.data, lease: account.lease }
};
})
);

return entries.reduce((acc, item) => Object.assign(acc, item), Object.create(null));
}

async function setBalance(nodeUrl: string, recipient: string, amount: number, assetId?: string): Promise<void> {
const tx = transfer({
recipient,
amount,
assetId,
additionalFee: 0.004 * Math.pow(10, 8)
} as any, MASTER_ACCOUNT_SEED);
await broadcastAndWait(nodeUrl, tx);
}

async function setLeasing(nodeUrl: string, recipient: string, amount: number): Promise<void> {
const tx = lease({
chainId: CHAIN_ID,
recipient,
amount,
additionalFee: 0.004 * Math.pow(10, 8)
}, MASTER_ACCOUNT_SEED);
await broadcastAndWait(nodeUrl, tx);
}

async function addScript(nodeUrl: string, seed: string, script: string): Promise<void> {
const tx = setScript({
chainId: CHAIN_ID,
script,
additionalFee: 0.004 * Math.pow(10, 8)
}, seed);
await broadcastAndWait(nodeUrl, tx);
}
40 changes: 40 additions & 0 deletions test/setup/createAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { issue } from '@waves/waves-transactions';
import { CHAIN_ID, MASTER_ACCOUNT_SEED, SMART_ASSET_SCRIPT } from './constants';
import { broadcastAndWait } from './utils';

interface IAsset {
name: string;
quantity?: number;
decimals?: number;
description?: string;
reissuable?: boolean;
script?: boolean | string;
owner?: string;
sponsorship?: boolean;
}

export default async function createAssets(
nodeUrl: string,
assets: Record<string, IAsset>,
accounts: Record<string, { seed: string }>
): Promise<Record<string, any>> {
const entries = await Promise.all(
Object.entries(assets).map(async ([key, asset]) => {
console.log(`Create asset ${key}`);
const tx = issue({
chainId: CHAIN_ID,
script: typeof asset.script === 'boolean' ? SMART_ASSET_SCRIPT : asset.script,
name: asset.name,
description: asset.description || `${asset.name} description`,
reissuable: asset.reissuable || false,
quantity: asset.quantity || 1000000 * Math.pow(10, 8),
decimals: typeof asset.decimals === 'number' && asset.decimals >= 0 ? asset.decimals : 8
}, asset.owner ? accounts[asset.owner].seed : MASTER_ACCOUNT_SEED);

await broadcastAndWait(nodeUrl, tx);
return { [key]: tx };
})
);

return entries.reduce((acc, item) => Object.assign(acc, item), Object.create(null));
}
Loading