diff --git a/README.md b/README.md index 8a3b261..56a14b2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # u-js-sample -A minimal network smoke test using `universa-core2@2.0.0-alpha.1`. +A minimal network and custom-token sample using `universa-core2@2.0.0-alpha.1`. ## Initialize and test @@ -9,6 +9,14 @@ npm ci scripts/test-network.sh ``` -The test loads `src/universa.json`, initializes `Network`, connects to the -available topology, and asserts that the network returns `sping=spong`. The -alpha package intentionally has no bundled default topology. +The test: + +1. Loads `src/universa.json` and the network-specific whitelisted client key. +2. Initializes `Network`, connects, and asserts that it returns `sping=spong`. +3. Creates and signs a simple `SAMPLE` token contract with split/join and + change-owner permissions. +4. Packs the contract into a `TransactionPack`, registers it without a fee, + and asserts that its final network state is `APPROVED`. + +The bundled client key is intended only for this test network. The alpha +package intentionally has no bundled default topology. diff --git a/package.json b/package.json index faaeebf..e952521 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "u-js-sample", "version": "0.1.0", - "description": "A minimal universa-core2 network smoke test.", + "description": "A minimal universa-core2 network and token registration sample.", "type": "module", "private": true, "scripts": { diff --git a/src/index.js b/src/index.js index 0962f25..51a8e34 100644 --- a/src/index.js +++ b/src/index.js @@ -5,21 +5,52 @@ import { readFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { + ChangeOwnerPermission, + Contract, Network, PrivateKey, + RoleSimple, + SplitJoinPermission, Topology, + TransactionPack, unicryptoReady } from 'universa-core2'; const sourceDirectory = dirname(fileURLToPath(import.meta.url)); const topologyFile = resolve(sourceDirectory, 'universa.json'); +const clientKeyFile = resolve(sourceDirectory, 'white.private.unikey'); + +const sleep = (milliseconds) => new Promise((resolvePromise) => { + setTimeout(resolvePromise, milliseconds); +}); + +async function waitForFinalState(network, connection, contractId) { + const deadline = Date.now() + 60_000; + + while (Date.now() < deadline) { + const { itemResult } = await network.getState( + contractId.composite3, + connection, + { timeout: 5_000 } + ); + const state = itemResult?.state; + + if (state && state !== 'UNDEFINED' && !state.startsWith('PENDING') && !state.startsWith('LOCKED')) { + return itemResult; + } + + await sleep(250); + } + + throw new Error('contract registration timed out'); +} await unicryptoReady; const topologyData = JSON.parse(await readFile(topologyFile, 'utf8')); const topology = await Topology.load(topologyData); -const privateKey = await PrivateKey.generate({ strength: 2048 }); -const network = new Network(privateKey, { +const clientKey = await PrivateKey.unpack(await readFile(clientKeyFile)); +const network = new Network(clientKey, { topology, directConnection: true }); @@ -31,3 +62,58 @@ const response = await network.command('sping'); assert.equal(response?.sping, 'spong', 'unexpected network state response'); console.log(`Universa network is functioning: ${network.size()} nodes, sping=spong`); + +// This key owns and signs the sample token. The white key above is used only +// to authenticate the client and authorize fee-free registration. +const tokenKey = await PrivateKey.generate({ strength: 2048 }); +const issuer = new RoleSimple('issuer', { + addresses: [tokenKey.publicKey.shortAddress] +}); +const splitJoinPermission = SplitJoinPermission.create('owner', { + field_name: 'amount', + min_value: '0', + min_unit: '0.01', + join_match_fields: ['state.origin'] +}); + +const token = Contract.create(issuer, { + definitionData: { + template_name: 'UNIT_CONTRACT', + unit_name: 'Sample Token', + unit_short_name: 'SAMPLE', + description: 'A token created by the universa-core2 sample' + }, + stateData: { + amount: '100' + }, + permissions: [ + ChangeOwnerPermission.create('owner'), + splitJoinPermission + ], + expiresAt: '3m', + createdAt: new Date() +}); + +await token.sign(tokenKey); +const transactionPack = new TransactionPack(token.pack()); +const packedTransaction = await transactionPack.pack(); +const contractId = await token.hashId(); + +console.log(`Created and packed SAMPLE token (${packedTransaction.length} bytes)`); + +const connection = await network.getRandomConnection(); +await network.command( + 'approve', + { packedItem: packedTransaction }, + connection, + { timeout: 10_000 } +); + +const itemResult = await waitForFinalState(network, connection, contractId); +assert.equal( + itemResult.state, + 'APPROVED', + `token registration failed: ${JSON.stringify(itemResult.errors ?? [])}` +); + +console.log(`SAMPLE token is ${itemResult.state}`); diff --git a/src/white.private.unikey b/src/white.private.unikey new file mode 100644 index 0000000..fb10f68 Binary files /dev/null and b/src/white.private.unikey differ