feature: basic zano utils js methods

This commit is contained in:
BadVoice 2025-06-14 11:38:55 +03:00
parent 49e15a223a
commit f73cc45c39
44 changed files with 10482 additions and 0 deletions

110
.eslintrc.json Normal file
View file

@ -0,0 +1,110 @@
{
"env": {
"es2017": true,
"node": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": [
"jest",
"@typescript-eslint",
"eslint-plugin-tsdoc",
"import",
"import-newlines"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:jest/all"
],
"rules": {
"brace-style": ["error", "1tbs"],
"comma-dangle": ["error", "always-multiline"],
"quotes": ["error", "single", {"avoidEscape": true}],
"import-newlines/enforce": [
"error",
{
"items": 2,
"max-len": 100,
"semi": false
}
],
"no-multiple-empty-lines": [
"error", {
"max": 2,
"maxEOF": 0,
"maxBOF": 0
}
],
"indent": ["error", 2, {"SwitchCase": 1}],
"object-curly-spacing": ["error", "always"],
"object-curly-newline": [
"error", {
"ObjectExpression": {
"minProperties": 4, "multiline": true, "consistent": true
},
"ObjectPattern": {
"minProperties": 4, "multiline": true, "consistent": true
},
"ImportDeclaration": {
"minProperties": 4, "multiline": true, "consistent": true
},
"ExportDeclaration": {
"minProperties": 4, "multiline": true, "consistent": true
}
}
],
"curly": [
"error",
"all"
],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/member-delimiter-style": "error",
"tsdoc/syntax": "warn",
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
],
"jest/no-hooks": "off",
"jest/prefer-expect-assertions": "off",
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"jest/no-disabled-tests": "off",
"jest/no-conditional-in-test": "off"
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
"project": "tsconfig.json"
}
}
}
}

47
.gitignore vendored Normal file
View file

@ -0,0 +1,47 @@
# build
/dist
/node_modules
.npmrc
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
/.idea/*
.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.env
*.properties
*.env.json
# Yarn >=2
.yarn
.pnp.*
.yarnrc.yml

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
18

425
README.md
View file

@ -1,2 +1,427 @@
# zano-utils-js
Set of helpers and tools for JS developers that working with Zano
<!-- TOC -->
* [Zano Utils JS](#zano-utils-js)
* [Blockchain Description](#blockchain-description)
* [Functions](#functions)
* [deocdeTransaction](#decodetransaction)
* [generateAccount](#generateaccount)
* [validateAccount](#validateaccount)
* [generateAccountKey](#generateaccountkeys)
* [privateKeyToPublicKey](#privatekeytopublickey)
* [getAccountBySecretSpendKey](#getaccountbysecretspendkey)
* [getKeyPair](#getkeypair)
* [getIntegratedAddress](#getintegratedaddress)
* [createIntegratedAddress](#createintegratedaddress)
* [getMasterAddress](#getmasteraddress)
* [splitIntegratedAddress](#splitintegratedaddress)
* [getKeysFromAddress](#getkeysfromaddress)
* [generatePaymentId](#generatepaymentid)
* [mnemonicToSeed](#mnemonictoseed)
* [seedToMnemonic](#seedtomnemonic)
<!-- TOC -->
## Blockchain Description
- **Ticker**: Zano
- **Network**: Zano
---
## Functions
### `decodeTransaction`
#### Node Method Used:
- `get_tx_details`
#### Example Request:
```bash
curl -X POST \
-H "Content-Type: application/json" \
--data '{ "jsonrpc": "2.0", "method": "get_tx_details", "params": {"tx_hash": "77b09d759fefd512642f9a5e4e31ed0fefbaf1a8e602a2be94fc511ff982f7cf" }, "id": 1 }' \
"http://37.27.100.59:10500/json_rpc"
```
<details>
<summary>Example response</summary>
```json
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"status": "OK",
"tx_info": {
"amount": 0,
"blob": "AwIlEBqFoRgAAAAAABp....",
"blob_size": 2017,
"extra": [{
"details_view": "",
"short_view": "(encrypted)",
"type": "payer"
},{
"details_view": "",
"short_view": "631a0aa63dd0caa6473f8a136a155f54a05d1f2f8b5b92c5145383be2a696834",
"type": "pub_key"
},{
"details_view": "0000",
"short_view": "0000",
"type": "FLAGS16"
},{
"details_view": "derivation_hash: 5fd099a9\nencrypted_key_derivation: 2fecfc1742ea69e8b25f76c5014bd2cb9ca249e2b717bbc6376e0196e450eccf",
"short_view": "derivation_hash: 5fd099a9",
"type": "crypto_checksum"
},{
"details_view": "ef40",
"short_view": "ef40",
"type": "derivation_hint"
},{
"details_view": "94c3",
"short_view": "94c3",
"type": "derivation_hint"
},{
"details_view": "fee = 0.01",
"short_view": "fee = 0.01",
"type": "zarcanum_tx_data_v1"
}],
"fee": 10000000000,
"id": "fd93234ee26299d9bcd0c7ffd66cfd31f66c16b7cd28776ddb3113f77477a669",
"ins": [{
"amount": 0,
"global_indexes": [1614213,1625091,1701687,1724120,1755061,1852550,1885364,1888520,1890757,1901665,1901797,1902935,1903361,1903474,1903878,1904042],
"htlc_origin": "",
"kimage_or_ms_id": "5eb5744c8fa2e2b44413323cc1b9bb6021c18218eb25f4c3a12c8ba5f8ca9e05",
"multisig_count": 0
},{
"amount": 0,
"global_indexes": [1415271,1478600,1553576,1658538,1872595,1899601,1901346,1901642,1902588,1903284,1903353,1903522,1903584,1903697,1903952,1903954],
"htlc_origin": "",
"kimage_or_ms_id": "e2a6d2a56c671fec431fa3025e8143ec2e9c14b1bdfc19936f6377c84abc0672",
"multisig_count": 0
}],
"keeper_block": 3199720,
"object_in_json": "ewogICJBR0dSRUdBVEVE....AgfQogICAgfQogIF0KfQ==",
"outs": [{
"amount": 0,
"global_index": 1904095,
"is_spent": false,
"minimum_sigs": 0,
"pub_keys": ["a08e213373e34988600db3168be88eff5a9e2da9972dcc12d6e51dc56a7b845b","23b5f778966dc4d8594549670a0335c8f639f18a39063606a9ebf1a826bb6a66","e22c32eb7fd87d8403d1516a5849966a1f8f4053eb56ae3a7048c3e5ffcaa0a2","a23fe6c894dc069ba92a252611027bcaae8c80d0848ce0e3357e05e61aa78914","5df242cb3fb59a87"]
},{
"amount": 0,
"global_index": 1904096,
"is_spent": false,
"minimum_sigs": 0,
"pub_keys": ["492d93dc0fb07d90e5d1ce9210b31fc1155206473d5d3f5136f7031ff6aad31e","6c24323a8e2690482cdd685956904168cb5fc8749f3be2df58f7d0aeb39f5252","bbaa2f259d5f6f5c1e0df43c9cbd7c061ac023f91ebb3fe22a5352115f642cec","d4ec19288fc18a81917c3328d45464bddfb04d5582690bc50062dac9dd963887","8dfdd23882a0a1ae"]
}],
"pub_key": "631a0aa63dd0caa6473f8a136a155f54a05d1f2f8b5b92c5145383be2a696834",
"timestamp": 1749725356
}
}
}
```
</details>
---
### `decodeTransaction`
Decode a transaction object_in_json using the provided secret view key and either address or public spend key.
#### Import and usage
```ts
import { decodeTransaction } from '@hyle-team/zano-utils-js';
import type { DecodeTransactionResult } from '@hyle-team/zano-utils-js';
decodeTransaction(objectInJson, secretViewKey, address);
or
decodeTransaction(objectInJson, secretViewKey,
publicSpendKey);
```
#### Returned data decodeTransaction
```ts
type DecodeTransactionResult =
| { ok: true; amount: string; paymentId?: string }
| { ok: false; error: string }
```
---
### `generateAccount`
```typescript
import { generateAccount } from '@hyle-team/zano-utils-js';
import type { AccountResult } from '@hyle-team/zano-utils-js';
const account: AccountResult = generateAccount();
```
#### Returned data generateAccount
```ts
type AccountResult = AccountKeys & {
address: string;
}
type AccountKeys = {
secretSpendKey: string;
secretViewKey: string;
publicSpendKey: string;
publicViewKey: string;
}
```
---
### `validateAccount`
```ts
import { validateAccount } from '@hyle-team/zano-utils-js';
const validatedAccount: boolean = validateAccount(
'ZxC15vh38qHAZbfsUXTpxoiyeLhavbBzsQQk81fEwP4jYxN4qR8SEhMfXkRBpQw6vbbSEGpK2VPVPADnL6h3ZViL29Remh4oH',
'21dcd98fb9dc392aeabe1d5cfb90faf63840685762448bf49a48d58d0c70bf0b',
'2ff9e77456d0e65b50d80392a098cddf9032744bd876371fffe95476a92d8564',
'88609e3bc954fe8b5f1a5f0a7e7e44528835b62890de49000033b28898888d01',
'b35fb46128f7150ecff93e0eeee80a95ad9b13e3bfced7d3ff7a121f6748df0e',
);
```
#### validateAccount params
```ts
async function validateAccount(
address: string,
publicSpendKey: string,
publicViewKey: string,
secretSpendKey: string,
secretViewKey: string,
): Promise<boolean>
```
---
### `generateAccountKeys`
```ts
import { generateAccountKeys } from '@hyle-team/zano-utils-js';
import type { AccountKeys } from '@hyle-team/zano-utils-js';
const accountKeys: AccountKeys = generateAccountKeys();
```
#### Returned data generateAccountKeys
```ts
type AccountKeys = {
secretSpendKey: string;
secretViewKey: string;
publicSpendKey: string;
publicViewKey: string;
}
```
---
### `privateKeyToPublicKey`
```ts
import { privateKeyToPublicKey } from '@hyle-team/zano-utils-js';
const publicKey: string = privateKeyToPublicKey('88609e3bc954fe8b5f1a5f0a7e7e44528835b62890de49000033b28898888d01');
```
---
### `getAccountBySecretSpendKey`
```ts
import { getAccountBySecretSpendKey } from '@hyle-team/zano-utils-js';
import type { AccountKeys } from '@hyle-team/zano-utils-js';
const accountKeys: AccountKeys = getAccountBySecretSpendKey('88609e3bc954fe8b5f1a5f0a7e7e44528835b62890de49000033b28898888d01');
```
#### Returned data getAccountBySecretSpendKey
```ts
type AccountKeys = {
secretSpendKey: string;
secretViewKey: string;
publicSpendKey: string;
publicViewKey: string;
}
```
---
### `getKeyPair`
```ts
import { getKeyPair } from '@hyle-team/zano-utils-js';
import type { KeyPair } from '@hyle-team/zano-utils-js';
const keypair: KeyPair = getKeyPair();
```
#### Returned data getKeyPair
```ts
type KeyPair = {
rawSecretKey: string;
secretKey: string;
publicKey: string;
}
```
---
### `getIntegratedAddress`
The function accepts either the main master address or the integrated address as a parameter.
```ts
import { getIntegratedAddress } from '@hyle-team/zano-utils-js';
const integratedAddress: string = getIntegratedAddress('ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH');
```
---
### `createIntegratedAddress`
```ts
import { createIntegratedAddress } from '@hyle-team/zano-utils-js';
const integratedAddress: string = createIntegratedAddress('ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH', '49c925855b863a25');
```
---
### `getMasterAddress`
params:
1. spendPublicKey: string,
2. viewPublicKey: string
```ts
import { getMasterAddress } from '@hyle-team/zano-utils-js';
const integratedAddress: string = getMasterAddress('9f5e1fa93630d4b281b18bb67a3db79e9622fc703cc3ad4a453a82e0a36d51fa', 'a3f208c8f9ba49bab28eed62b35b0f6be0a297bcd85c2faa1eb1820527bcf7e3');
```
---
### `splitIntegratedAddress`
Descr: Extract paymentId and master address from integrated address
params:
1. integratedAddress: string,
```ts
import { splitIntegratedAddress } from '@hyle-team/zano-utils-js';
import type { SplitedIntegratedAddress } from '@hyle-team/zano-utils-js';
const integratedAddress: SplitedIntegratedAddress = splitIntegratedAddress('iZ2kFmwxRHoaRxm1ni8HnfUTkYuKbni8s4CE2Z4GgFfH99BJ6cnbAtJTgUnZjPj9CTCTKy1qqM9wPCTp92uBC7e47JPoHxGL5UU2D1tpQMg4');
```
#### Returned data splitIntegratedAddress
```ts
type SplitedIntegratedAddress = {
masterAddress: string;
paymentId: string;
}
```
---
### `getKeysFromAddress`
Descr: Extract public keys from master or integratedAddress
params:
1. address: string,
```ts
import { getKeysFromAddress } from '@hyle-team/zano-utils-js';
import type { ZarcanumAddressKeys } from '@hyle-team/zano-utils-js';
const integratedAddress: ZarcanumAddressKeys = getKeysFromAddress('iZ2kFmwxRHoaRxm1ni8HnfUTkYuKbni8s4CE2Z4GgFfH99BJ6cnbAtJTgUnZjPj9CTCTKy1qqM9wPCTp92uBC7e47JPoHxGL5UU2D1tpQMg4');
```
#### Returned data getKeysFromAddress
```ts
type ZarcanumAddressKeys = {
spendPublicKey: string;
viewPublicKey: string;
};
```
---
### `generatePaymentId`
Descr: generate payment id for creating integrated address
```ts
import { generatePaymentId } from '@hyle-team/zano-utils-js';
const paymentId: string = generatePaymentId();
```
---
### `mnemonicToSeed`
## Limitations
- **No support for password-protected seed phrases**: Currently, the library does not handle mnemonic phrases that are encrypted with a password.
- **No audit flag support**: The library does not yet support the audit flag feature.
@param seedPhraseRaw - Raw seed phrase string (mnemonic) containing 25 or 26 words.
@returns The secret spend key as a hex string, or `false` if parsing failed.
```ts
import { mnemonicToSeed } from '@hyle-team/zano-utils-js';
import type { MnemonicToSeedResult } from '@hyle-team/zano-utils-js';
const secretSpendKey: MnemonicToSeedResult = mnemonicToSeed('bridge passion scale vast speak mud murder own birthday flight always hair especially tickle crowd shatter tickle deserve hopefully bomb join plan darling aunt beneath give');
```
#### Returned data mnemonicToSeed
```ts
type MnemonicToSeedResult = string | false;
```
---
### `seedToMnemonic`
## Warning:
**Dont use secret spend for creating mnemonic**
**You need provide raw seed key, its key using for creating secret spend key and mnemonic**
## Limitations
- **No support for password-protected seed phrases**: Currently, the library does not handle mnemonic phrases that are encrypted with a password.
- **No audit flag support**: The library does not yet support the audit flag feature.
```ts
import { seedToMnemonic } from '@hyle-team/zano-utils-js';
import type { SeedToMnemonicResult } from '@hyle-team/zano-utils-js';
const randomBytes: string = getRandomBytes(64).toString('hex');
const seedPhrase: SeedToMnemonicResult = mnemonicToSeed(randomBytes);
```
#### Returned data seedToMnemonic
```ts
type SeedToMnemonicResult = string;
```
<br>
<br>
# Supporting project/donations
---
ZANO @dev <br>
BTC bc1qpa8w8eaehlplfepmnzpd7v9j046899nktxnkxp <br>
BCH qqgq078vww5exd9kt3frx6krdyznmp80hcygzlgqzd <br>
ETH 0x206c52b78141498e74FF074301ea90888C40c178 <br>
XMR 45gp9WTobeB5Km3kLQgVmPJkvm9rSmg4gdyHheXqXijXYMjUY48kLgL7QEz5Ar8z9vQioQ68WYDKsQsjAEonSeFX4UeLSiX

76
package.json Normal file
View file

@ -0,0 +1,76 @@
{
"name": "@hyle-team/zano-utils-js",
"version": "0.0.1",
"repository": "https://github.com/hyle-team/zano-utils-js",
"author": "Zano Team",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"prebuild": "rimraf dist",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r ts-node/register node_modules/.bin/jest --runInBand",
"build": "tsc -p ./tsconfig.build.json",
"preversion": "yarn test",
"version": "yarn build && git add -A dist",
"postversion": "git push && git push --tags",
"lint": "eslint \"{src,apps,libs,test,lib}/**/*.ts\" --fix"
},
"dependencies": {
"bn.js": "^5.2.1",
"elliptic": "^6.5.4",
"js-sha3": "^0.9.3",
"keccak": "^3.0.3",
"axios": "^0.21.1",
"big.js": "^6.1.1",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@types/big.js": "^6",
"@types/elliptic": "^6.4.18",
"@types/keccak": "^3.0.5",
"@types/node": "^18",
"@types/jest": "^26.0.24",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"eslint": "^7.32.0",
"eslint-import-resolver-node": "^0.3.4",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-import-newlines": "^1.1.7",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-tsdoc": "^0.2.14",
"jest": "^26.6.3",
"ts-jest": "^26.5.6",
"ts-node": "^10.1.0",
"typescript": "^4.3.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": ".",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s",
"!dist/**",
"!coverage/**"
],
"coverageDirectory": "./coverage",
"testEnvironment": "node"
},
"packageManager": "yarn@1.22.22"
}

View file

@ -0,0 +1,170 @@
import { BRAINWALLET_DEFAULT_SEED_SIZE } from './constants';
import type {
AccountKeys,
AccountResult,
KeyPair,
SpendKeypair,
} from './types';
import { getKeysFromAddress, getMasterAddress } from '../address/address-utils';
import { ADDRESS_REGEX } from '../address/constants';
import type { ZarcanumAddressKeys } from '../address/types';
import {
dependentKey,
generateSeedKeys,
generateSeedKeysWeb,
secretKeyToPublicKey,
} from '../core/crypto';
function generateAccount(): AccountResult {
const keys: AccountKeys = generateAccountKeys();
if (!keys || !keys.secretSpendKey || !keys.publicSpendKey || !keys.secretViewKey || !keys.publicViewKey) {
throw new Error('Invalid generated keys');
}
const address: string = getMasterAddress(keys.publicSpendKey, keys.publicViewKey);
try {
validateAccount(address, keys.publicSpendKey, keys.publicViewKey, keys.secretSpendKey, keys.secretViewKey);
} catch (error) {
console.error('Error validating address:', error);
throw error.message;
}
return {
address,
...keys,
};
}
function validateAccount(
address: string,
publicSpendKey: string,
publicViewKey: string,
secretSpendKey: string,
secretViewKey: string,
): boolean {
if (!ADDRESS_REGEX.test(address)) {
throw new Error('invalid address format');
}
const { spendPublicKey }: ZarcanumAddressKeys = getKeysFromAddress(address);
if (spendPublicKey !== publicSpendKey) {
throw new Error('invalid address keys');
}
const secretSpendKeyBuf: Buffer = Buffer.from(secretSpendKey, 'hex');
const secViewKey: string = dependentKey(secretSpendKeyBuf);
if (secViewKey !== secretViewKey) {
throw new Error('invalid depend secret view key');
}
const secretViewKeyBuf: Buffer = Buffer.from(secretViewKey, 'hex');
const pubViewKey: string = secretKeyToPublicKey(secretViewKeyBuf);
if (pubViewKey !== publicViewKey) {
throw new Error('pub view key from secret key no equal provided pub view key');
}
const pubSpendKey: string = secretKeyToPublicKey(secretSpendKeyBuf);
if (pubSpendKey !== pubSpendKey) {
throw new Error('pub spend key from secret key no equal provided pub spend key');
}
return true;
}
function generateAccountKeys(): AccountKeys {
const {
secretSpendKey,
publicSpendKey,
}: SpendKeypair = generateSeedKeys(BRAINWALLET_DEFAULT_SEED_SIZE);
if (!secretSpendKey || !publicSpendKey) {
throw new Error('Error generate seed keys');
}
const secretSpendKeyBuf: Buffer = Buffer.from(secretSpendKey, 'hex');
const secretViewKey: string = dependentKey(secretSpendKeyBuf);
if (!secretViewKey) {
throw new Error('Error generate seed keys');
}
const secretViewKeyBuf: Buffer = Buffer.from(secretViewKey, 'hex');
const publicViewKey: string = secretKeyToPublicKey(secretViewKeyBuf);
return {
secretSpendKey,
publicSpendKey,
secretViewKey,
publicViewKey,
};
}
function getAccountBySecretSpendKey(secretSpendKey: string): AccountKeys {
if (secretSpendKey.length !== 64 || !/^([0-9a-fA-F]{2})+$/.test(secretSpendKey)) {
throw new Error('Invalid secret spend key');
}
const secretSpendKeyBuf: Buffer = Buffer.from(secretSpendKey, 'hex');
const secretViewKey: string = dependentKey(secretSpendKeyBuf);
const publicSpendKey: string = secretKeyToPublicKey(secretSpendKeyBuf);
if (!secretViewKey || !publicSpendKey) {
throw new Error('Error generate seed keys');
}
const secretViewKeyBuf: Buffer = Buffer.from(secretViewKey, 'hex');
const publicViewKey: string = secretKeyToPublicKey(secretViewKeyBuf);
return {
secretSpendKey,
publicSpendKey,
secretViewKey,
publicViewKey,
};
}
function privateKeyToPublicKey(secretKey: string): string {
if (secretKey.length !== 64 || !/^([0-9a-fA-F]{2})+$/.test(secretKey)) {
throw new Error('Invalid secret spend key');
}
const secretKeyBuf: Buffer = Buffer.from(secretKey, 'hex');
const publicKey: string = secretKeyToPublicKey(secretKeyBuf);
if (!publicKey) {
throw new Error('Error generate seed keys');
}
return publicKey;
}
function getKeyPair(): KeyPair {
const {
seedKey,
secretSpendKey,
publicSpendKey,
} = generateSeedKeysWeb(BRAINWALLET_DEFAULT_SEED_SIZE);
return {
rawSecretKey: seedKey,
secretKey: secretSpendKey,
publicKey: publicSpendKey,
};
}
export {
privateKeyToPublicKey,
generateAccount,
validateAccount,
generateAccountKeys,
getAccountBySecretSpendKey,
getKeyPair,
};

1
src/account/constants.ts Normal file
View file

@ -0,0 +1 @@
export const BRAINWALLET_DEFAULT_SEED_SIZE = 32;

22
src/account/types.ts Normal file
View file

@ -0,0 +1,22 @@
export type AccountKeys = {
secretSpendKey: string;
secretViewKey: string;
publicSpendKey: string;
publicViewKey: string;
}
export type AccountResult = AccountKeys & {
address: string;
}
export type SpendKeypair = {
seedKey?: string;
secretSpendKey: string;
publicSpendKey: string;
}
export type KeyPair = {
rawSecretKey: string;
secretKey: string;
publicKey: string;
}

View file

@ -0,0 +1,257 @@
import { ZarcanumAddressKeys } from 'src/decode/types';
import {
PAYMENT_ID_REGEX,
ADDRESS_TAG_PREFIX,
ADDRESS_FLAG_PREFIX,
BUFFER_INTEGRATED_ADDRESS_LENGTH,
INTEGRATED_ADDRESS_REGEX,
PAYMENT_ID_LENGTH,
INTEGRATED_ADDRESS_FLAG_PREFIX,
INTEGRATED_ADDRESS_TAG_PREFIX,
BUFFER_ADDRESS_LENGTH,
CHECKSUM_LENGTH,
FLAG_LENGTH,
SPEND_KEY_LENGTH,
TAG_LENGTH,
VIEW_KEY_LENGTH,
ADDRESS_REGEX,
ACCOUNT_KEY_REGEX,
} from './constants';
import { DecodedAddress, SplitedIntegratedAddress } from './types';
import { base58Encode, base58Decode } from '../core/base58';
import { getRandomBytes , getChecksum } from '../core/crypto';
function getIntegratedAddress(address: string): string {
return createIntegratedAddress(address, generatePaymentId());
}
function createIntegratedAddress(address: string, paymentId: string): string {
if (!validateAddress(address)) {
throw new Error('Invalid address format');
}
if (!validatePaymentId(paymentId)) {
throw new Error('Invalid payment ID format');
}
try {
const paymentIdBuffer: Buffer = Buffer.from(paymentId, 'hex');
const addressDecoded: DecodedAddress = decodeAddress(address);
if (!addressDecoded) {
return null;
}
return formatIntegratedAddress(addressDecoded, paymentIdBuffer);
} catch (error) {
throw new Error(`Error creating integrated address: ${error.message}`);
}
}
function formatIntegratedAddress(addressDecoded: DecodedAddress, paymentIdBuffer: Buffer): string {
const {
tag,
flag,
viewPublicKey,
spendPublicKey,
}: DecodedAddress = addressDecoded;
const integratedAddressBuffer: Buffer = Buffer.concat([
Buffer.from([tag, flag]),
viewPublicKey,
spendPublicKey,
paymentIdBuffer,
]);
const checksum: string = calculateChecksum(integratedAddressBuffer);
return base58Encode(Buffer.concat([integratedAddressBuffer, Buffer.from(checksum, 'hex')]));
}
function decodeAddress(address: string): DecodedAddress {
try {
const decodedAddress: Buffer = base58Decode(address);
if (!decodedAddress) {
throw new Error('Invalid decode address');
}
let offset = TAG_LENGTH + FLAG_LENGTH;
const viewPublicKey: Buffer = decodedAddress.subarray(offset, offset + VIEW_KEY_LENGTH);
offset += VIEW_KEY_LENGTH;
const spendPublicKey: Buffer = decodedAddress.subarray(offset, offset + SPEND_KEY_LENGTH);
return {
tag: INTEGRATED_ADDRESS_TAG_PREFIX,
flag: INTEGRATED_ADDRESS_FLAG_PREFIX,
viewPublicKey,
spendPublicKey,
};
} catch (error) {
throw new Error(`Error decode address: ${error.message}`);
}
}
function encodeAddress(tag: number, flag: number, spendPublicKey: string, viewPublicKey: string): string {
try {
if (tag < 0) {
throw new Error('Invalid tag');
}
if (flag < 0) {
throw new Error('Invalid flag');
}
let buf: Buffer = Buffer.from([tag, flag]);
if (spendPublicKey.length !== 64 && !ACCOUNT_KEY_REGEX.test(spendPublicKey)) {
throw new Error('Invalid spendPublicKey: must be a hexadecimal string with a length of 64');
}
const spendKey: Buffer = Buffer.from(spendPublicKey, 'hex');
if (viewPublicKey.length !== 64 && !ACCOUNT_KEY_REGEX.test(viewPublicKey)) {
throw new Error('Invalid viewPrivateKey: must be a hexadecimal string with a length of 64');
}
const viewKey: Buffer = Buffer.from(viewPublicKey, 'hex');
buf = Buffer.concat([buf, spendKey, viewKey]);
const hash: string = getChecksum(buf);
return base58Encode(Buffer.concat([buf, Buffer.from(hash, 'hex')]));
} catch (error) {
throw new Error(error.message);
}
}
function getMasterAddress(spendPublicKey: string, viewPublicKey: string): string {
try {
const tag: number = ADDRESS_TAG_PREFIX;
const flag: number = ADDRESS_FLAG_PREFIX;
if (spendPublicKey.length !== 64 && !ACCOUNT_KEY_REGEX.test(spendPublicKey)) {
throw new Error('Invalid spendPublicKey: must be a hexadecimal string with a length of 64');
}
if (viewPublicKey.length !== 64 && !ACCOUNT_KEY_REGEX.test(viewPublicKey)) {
throw new Error('Invalid viewPrivateKey: must be a hexadecimal string with a length of 64');
}
const viewPublicKeyBuf: Buffer = Buffer.from(viewPublicKey, 'hex');
const spendPublicKeyBuf: Buffer = Buffer.from(spendPublicKey, 'hex');
let buf: Buffer = Buffer.from([tag, flag]);
buf = Buffer.concat([buf, spendPublicKeyBuf, viewPublicKeyBuf]);
const hash: string = getChecksum(buf);
return base58Encode(Buffer.concat([buf, Buffer.from(hash, 'hex')]));
} catch (error) {
throw new Error(error.message);
}
}
function splitIntegratedAddress(integratedAddress: string): SplitedIntegratedAddress {
try {
if (!INTEGRATED_ADDRESS_REGEX.test(integratedAddress)) {
throw new Error('Invalid integratedAddress: must be a hexadecimal string with a length of 106 whit correct regex');
}
const {
spendPublicKey,
viewPublicKey,
}: ZarcanumAddressKeys = getKeysFromAddress(integratedAddress);
if (!spendPublicKey || !viewPublicKey) {
throw new Error('spendPublicKey or viewPublicKey are missing');
}
const paymentId: string = base58Decode(integratedAddress).subarray(66, 66 + PAYMENT_ID_LENGTH).toString('hex');
const masterAddress: string = getMasterAddress(spendPublicKey, viewPublicKey);
return {
masterAddress,
paymentId,
};
} catch (error) {
throw new Error(`Error decode integrated address: ${error.message}`);
}
}
/*
* Retrieves public spend and view keys from the Zano address.
*
* This function decodes a Zano address and Integrated address from its Base58 representation and extracts
* the spend and view keys contained within it. If the address is not in a valid
* Base58 format, or if the resulting buffer does not conform to expected length specifics,
* an error is thrown.
*
* @param {string} address - A Zano address and Integrated address in Base58 format.
* @returns { ZarcanumAddressKeys } An object containing the spend and view keys.
* @throws { Error } Throws an error if the address format or buffer length is invalid.
*/
function getKeysFromAddress(address: string): ZarcanumAddressKeys {
if (!ADDRESS_REGEX.test(address) && !INTEGRATED_ADDRESS_REGEX.test(address)) {
throw new Error('Invalid address format');
}
const buf: Buffer = base58Decode(address);
if (buf.length !== BUFFER_ADDRESS_LENGTH && buf.length !== BUFFER_INTEGRATED_ADDRESS_LENGTH) {
throw new Error('Invalid buffer address length');
}
const addressWithoutChecksum: Buffer = Buffer.from(buf.buffer, 0, buf.length - CHECKSUM_LENGTH);
const checksum: string = Buffer.from(buf.buffer, buf.length - CHECKSUM_LENGTH).toString('hex');
if (checksum !== getChecksum(addressWithoutChecksum)) {
throw new Error('Invalid address checksum');
}
const spendPublicKey: string = Buffer.from(
buf.buffer,
TAG_LENGTH + FLAG_LENGTH,
SPEND_KEY_LENGTH,
).toString('hex');
const viewPublicKey: string = Buffer.from(
buf.buffer,
TAG_LENGTH + FLAG_LENGTH + SPEND_KEY_LENGTH,
VIEW_KEY_LENGTH,
).toString('hex');
if (!spendPublicKey || spendPublicKey.length !== SPEND_KEY_LENGTH * 2 ||
!viewPublicKey || viewPublicKey.length !== VIEW_KEY_LENGTH * 2) {
throw new Error('Invalid key format in the address.');
}
return {
spendPublicKey,
viewPublicKey,
};
}
function generatePaymentId(): string {
return getRandomBytes(PAYMENT_ID_LENGTH).toString('hex');
}
function validatePaymentId(paymentId: string): boolean {
return PAYMENT_ID_REGEX.test(paymentId);
}
function validateAddress(address: string): boolean {
return INTEGRATED_ADDRESS_REGEX.test(address) || ADDRESS_REGEX.test(address);
}
function calculateChecksum(buffer: Buffer): string {
return getChecksum(buffer);
}
export {
getIntegratedAddress,
createIntegratedAddress,
encodeAddress,
getMasterAddress,
splitIntegratedAddress,
getKeysFromAddress,
generatePaymentId,
};

23
src/address/constants.ts Normal file
View file

@ -0,0 +1,23 @@
export const TAG_LENGTH = 1;
export const FLAG_LENGTH = 1;
export const SPEND_KEY_LENGTH = 32;
export const VIEW_KEY_LENGTH = 32;
export const CHECKSUM_LENGTH = 4;
export const BUFFER_ADDRESS_LENGTH: number =
TAG_LENGTH +
FLAG_LENGTH +
SPEND_KEY_LENGTH +
VIEW_KEY_LENGTH +
CHECKSUM_LENGTH;
export const ADDRESS_REGEX = /^Z[a-zA-Z0-9]{96}$/;
export const INTEGRATED_ADDRESS_REGEX = /^iZ[a-zA-Z0-9]{106}$/;
export const PAYMENT_ID_LENGTH = 8;
export const INTEGRATED_ADDRESS_FLAG_PREFIX = 0x6c;
export const INTEGRATED_ADDRESS_TAG_PREFIX = 0xf8;
export const ADDRESS_FLAG_PREFIX = 0x01;
export const ADDRESS_TAG_PREFIX = 0xC5;
export const BUFFER_INTEGRATED_ADDRESS_LENGTH =
BUFFER_ADDRESS_LENGTH +
PAYMENT_ID_LENGTH;
export const PAYMENT_ID_REGEX = /^[a-fA-F0-9]{16}$/;
export const ACCOUNT_KEY_REGEX = /^([0-9a-fA-F]{2})+$/;

16
src/address/types.ts Normal file
View file

@ -0,0 +1,16 @@
export type ZarcanumAddressKeys = {
spendPublicKey: string;
viewPublicKey: string;
}
export type DecodedAddress = {
tag: number;
flag: number;
viewPublicKey: Buffer;
spendPublicKey: Buffer;
}
export type SplitedIntegratedAddress = {
masterAddress: string;
paymentId: string;
}

254
src/core/arx.ts Normal file
View file

@ -0,0 +1,254 @@
export type TypedArray = Int8Array | Uint8ClampedArray | Uint8Array |
Uint16Array | Int16Array | Uint32Array | Int32Array;
export const u32 = (arr: TypedArray) =>
new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
export function clean(...arrays: TypedArray[]) {
for (let i = 0; i < arrays.length; i++) {
arrays[i].fill(0);
}
}
export function copyBytes(bytes: Uint8Array) {
return Uint8Array.from(bytes);
}
const _utf8ToBytes = (str: string) => Uint8Array.from(str.split('').map((c) => c.charCodeAt(0)));
const sigma16 = _utf8ToBytes('expand 16-byte k');
const sigma32 = _utf8ToBytes('expand 32-byte k');
const sigma16_32 = u32(sigma16);
const sigma32_32 = u32(sigma32);
export function rotl(a: number, b: number): number {
return (a << b) | (a >>> (32 - b));
}
export type CipherCoreFn = (
sigma: Uint32Array,
key: Uint32Array,
nonce: Uint32Array,
output: Uint32Array,
counter: number,
rounds?: number
) => void;
export type ExtendNonceFn = (
sigma: Uint32Array,
key: Uint32Array,
input: Uint32Array,
output: Uint32Array
) => void;
export type CipherOpts = {
allowShortKeys?: boolean; // Original salsa / chacha allow 16-byte keys
extendNonceFn?: ExtendNonceFn;
counterLength?: number;
counterRight?: boolean; // right: nonce|counter; left: counter|nonce
rounds?: number;
};
// Is byte array aligned to 4 byte offset (u32)?
function isAligned32(b: Uint8Array) {
return b.byteOffset % 4 === 0;
}
// Salsa and Chacha block length is always 512-bit
const BLOCK_LEN = 64;
const BLOCK_LEN32 = 16;
// new Uint32Array([2**32]) // => Uint32Array(1) [ 0 ]
// new Uint32Array([2**32-1]) // => Uint32Array(1) [ 4294967295 ]
const MAX_COUNTER = 2 ** 32 - 1;
const U32_EMPTY = new Uint32Array();
function runCipher(
core: CipherCoreFn,
sigma: Uint32Array,
key: Uint32Array,
nonce: Uint32Array,
data: Uint8Array,
output: Uint8Array,
counter: number,
rounds: number,
): void {
const len = data.length;
const block = new Uint8Array(BLOCK_LEN);
const b32 = u32(block);
// Make sure that buffers aligned to 4 bytes
const isAligned = isAligned32(data) && isAligned32(output);
const d32 = isAligned ? u32(data) : U32_EMPTY;
const o32 = isAligned ? u32(output) : U32_EMPTY;
for (let pos = 0; pos < len; counter++) {
core(sigma, key, nonce, b32, counter, rounds);
if (counter >= MAX_COUNTER) {
throw new Error('arx: counter overflow');
}
const take = Math.min(BLOCK_LEN, len - pos);
// aligned to 4 bytes
if (isAligned && take === BLOCK_LEN) {
const pos32 = pos / 4;
if (pos % 4 !== 0) {
throw new Error('arx: invalid block position');
}
for (let j = 0, posj: number; j < BLOCK_LEN32; j++) {
posj = pos32 + j;
o32[posj] = d32[posj] ^ b32[j];
}
pos += BLOCK_LEN;
continue;
}
for (let j = 0, posj; j < take; j++) {
posj = pos + j;
output[posj] = data[posj] ^ block[j];
}
pos += take;
}
}
type EmptyObj = {};
export function checkOpts<T1 extends EmptyObj, T2 extends EmptyObj>(
defaults: T1,
opts: T2,
): T1 & T2 {
if (opts == null || typeof opts !== 'object') {
throw new Error('options must be defined');
}
const merged = Object.assign(defaults, opts);
return merged as T1 & T2;
}
export type XorStream = (
key: Uint8Array,
nonce: Uint8Array,
data: Uint8Array,
output?: Uint8Array,
counter?: number
) => Uint8Array;
function anumber(n: number) {
if (!Number.isSafeInteger(n) || n < 0) {
throw new Error('positive integer expected, got');
}
}
function abool(b: boolean) {
if (typeof b !== 'boolean') {
throw new Error(`boolean expected, not ${b}`);
}
}
function abytes(b: Uint8Array | undefined, ...lengths: number[]) {
if (!isBytes(b)) {
throw new Error('Uint8Array expected');
}
if (lengths.length > 0 && !lengths.includes(b.length)) {
throw new Error('Uint8Array expected of length');
}
}
function isBytes(a: unknown): a is Uint8Array {
return a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array');
}
export function createCipher(core: CipherCoreFn, opts: CipherOpts): XorStream {
const {
allowShortKeys, extendNonceFn, counterLength, counterRight, rounds,
} = checkOpts(
{
allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20,
},
opts,
);
if (typeof core !== 'function') {
throw new Error('core must be a function');
}
anumber(counterLength);
anumber(rounds);
abool(counterRight);
abool(allowShortKeys);
return (
key: Uint8Array,
nonce: Uint8Array,
data: Uint8Array,
output?: Uint8Array,
counter = 0,
): Uint8Array => {
abytes(key);
abytes(nonce);
abytes(data);
const len = data.length;
if (output === undefined) {
output = new Uint8Array(len);
}
abytes(output);
anumber(counter);
if (counter < 0 || counter >= MAX_COUNTER) {
throw new Error('arx: counter overflow');
}
if (output.length < len) {
throw new Error(`arx: output (${output.length}) is shorter than data (${len})`);
}
const toClean = [];
// Key & sigma
// key=16 -> sigma16, k=key|key
// key=32 -> sigma32, k=key
const l = key.length;
let k: Uint8Array;
let sigma: Uint32Array;
if (l === 32) {
toClean.push((k = copyBytes(key)));
sigma = sigma32_32;
} else if (l === 16 && allowShortKeys) {
k = new Uint8Array(32);
k.set(key);
k.set(key, 16);
sigma = sigma16_32;
toClean.push(k);
} else {
throw new Error(`arx: invalid 32-byte key, got length=${l}`);
}
// Nonce
// salsa20: 8 (8-byte counter)
// chacha20orig: 8 (8-byte counter)
// chacha20: 12 (4-byte counter)
// xsalsa20: 24 (16 -> hsalsa, 8 -> old nonce)
// xchacha20: 24 (16 -> hchacha, 8 -> old nonce)
// Align nonce to 4 bytes
if (!isAligned32(nonce)) {
toClean.push((nonce = copyBytes(nonce)));
}
const k32 = u32(k);
// hsalsa & hchacha: handle extended nonce
if (extendNonceFn) {
if (nonce.length !== 24) {
throw new Error('arx: extended nonce must be 24 bytes');
}
extendNonceFn(sigma, k32, u32(nonce.subarray(0, 16)), k32);
nonce = nonce.subarray(16);
}
// Handle nonce counter
const nonceNcLen = 16 - counterLength;
if (nonceNcLen !== nonce.length) {
throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`);
}
// Pad counter when nonce is 64 bit
if (nonceNcLen !== 12) {
const nc = new Uint8Array(12);
nc.set(nonce, counterRight ? 0 : 12 - nonce.length);
nonce = nc;
toClean.push(nonce);
}
const n32 = u32(nonce);
runCipher(core, sigma, k32, n32, data, output, counter, rounds);
clean(...toClean);
return output;
};
}

146
src/core/base58.ts Normal file
View file

@ -0,0 +1,146 @@
const UINT64_MAX = 2n ** 64n - 1n;
const UINT64_SIZE = 8;
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const ALPHABET_SIZE = BigInt(ALPHABET.length);
const ENCODED_BLOCK_SIZES = [0, 2, 3, 5, 6, 7, 9, 10, 11];
const FULL_DECODED_BLOCK_SIZE = ENCODED_BLOCK_SIZES.length - 1;
const FULL_ENCODED_BLOCK_SIZE = ENCODED_BLOCK_SIZES[FULL_DECODED_BLOCK_SIZE];
const REVERSE_ALPHABET = [];
for (let i = 0; i < ALPHABET.length; i++) {
REVERSE_ALPHABET[ALPHABET.charCodeAt(i) - ALPHABET.charCodeAt(0)] = i;
}
const DECODED_BLOCK_SIZES = [];
for (let i = 0; i <= FULL_DECODED_BLOCK_SIZE; i++) {
DECODED_BLOCK_SIZES[ENCODED_BLOCK_SIZES[i]] = i;
}
function bufferToUint64(buffer: Uint8Array) {
if (!buffer.length || buffer.length > UINT64_SIZE) {
throw new Error(
`only a buffer of size between 1 and ${UINT64_SIZE} can be converted `,
);
}
let uint64 = 0n;
for (let i = 0; i < buffer.length; i++) {
uint64 = (uint64 << 8n) + BigInt(buffer[i]);
}
return uint64;
}
function uint64ToBuffer(buffer: Buffer, uint64: bigint): void {
if (!buffer.length || buffer.length > UINT64_SIZE) {
throw new Error(
'a uint64 can only be converted to a buffer of size between 1 and ' +
`${UINT64_SIZE}`,
);
}
for (let i = buffer.length - 1; uint64; i--) {
if (i < 0) {
throw new Error(
'buffer size insufficient to represent the uint64',
);
}
buffer[i] = Number(uint64 & 0xffn);
uint64 >>= 8n;
}
}
export function encodeBlock(buffer: Uint8Array): string {
if (!buffer.length || buffer.length > FULL_ENCODED_BLOCK_SIZE) {
throw new Error(
'base58 block buffer size must be between 1 and ' +
`${FULL_ENCODED_BLOCK_SIZE}`,
);
}
const stringSize = ENCODED_BLOCK_SIZES[buffer.length];
let uint64 = bufferToUint64(buffer);
let string = '';
for (let i = stringSize - 1; uint64 && i >= 0; i--) {
const rem = Number(uint64 % ALPHABET_SIZE);
uint64 /= ALPHABET_SIZE;
string = ALPHABET[rem] + string;
}
return string.padStart(stringSize, ALPHABET[0]);
}
export function decodeBlock(buffer: Buffer, string: string): void {
if (!string || string.length > FULL_ENCODED_BLOCK_SIZE) {
throw new Error(
'base58 block string size must be between 1 and ' +
`${FULL_ENCODED_BLOCK_SIZE}`,
);
}
let uint64 = 0n;
for (let i = 0; i < string.length; i++) {
const digit = REVERSE_ALPHABET[string.charCodeAt(i) - ALPHABET.charCodeAt(0)];
if (digit === undefined) {
throw new Error(
'base58 string block contains invalid character',
);
}
uint64 = uint64 * ALPHABET_SIZE + BigInt(digit);
}
if (uint64 > UINT64_MAX) {
throw new Error(
'numeric value of base58 block string overflows uint64',
);
}
uint64ToBuffer(buffer, uint64);
}
export function base58Encode(buffer: Uint8Array): string {
let string = '';
for (let start = 0; start < buffer.length;) {
const end: number = start + FULL_DECODED_BLOCK_SIZE;
const block: Uint8Array = buffer.subarray(start, end);
string += encodeBlock(block);
start = end;
}
return string;
}
export function base58Decode(string: string): Buffer {
const bufferSize: number = Math.floor(string.length / FULL_ENCODED_BLOCK_SIZE) *
FULL_DECODED_BLOCK_SIZE +
+DECODED_BLOCK_SIZES[string.length % FULL_ENCODED_BLOCK_SIZE];
if (!bufferSize) {
throw new Error(
'base58 string has an invalid size',
);
}
const buffer = Buffer.alloc(bufferSize);
for (let startEncoded = 0, startDecoded = 0; startEncoded < string.length;) {
const endDecoded: number = startDecoded + FULL_DECODED_BLOCK_SIZE;
const blockDecoded: Buffer = buffer.subarray(startDecoded, endDecoded);
const endEncoded: number = startEncoded + FULL_ENCODED_BLOCK_SIZE;
const blockEncoded: string = string.slice(startEncoded, endEncoded);
decodeBlock(blockDecoded, blockEncoded);
startDecoded = endDecoded;
startEncoded = endEncoded;
}
return buffer;
}

View file

@ -0,0 +1,70 @@
export class BinaryArchive {
public _buffer: Buffer;
public _offset: number;
constructor(buffer: Buffer) {
this._buffer = buffer;
this._offset = 0;
}
get offset(): number {
return this._offset;
}
eof(): boolean {
return this._offset === this._buffer.length;
}
readUint8(): number {
const value: number = this._buffer.readUInt8(this._offset);
this._offset += 1;
return value;
}
readUint16(): bigint {
const value = BigInt(this._buffer.readUInt16LE(this._offset));
this._offset += 2;
return value;
}
readUint32(): bigint {
const value = BigInt(this._buffer.readUInt32LE(this._offset));
this._offset += 4;
return value;
}
/**
*
* @see https://github.com/hyle-team/zano/blob/69a5d42d9908b7168247e103b2b40aae8c1fb3f5/src/common/varint.h#L59
*/
readVarint(): bigint {
let varint = 0n;
let shift = 0;
let byte: number;
do {
byte = this.readUint8();
varint |= BigInt(byte & 0x7f) << BigInt(shift);
shift += 7;
if (shift >= 64) {
throw new Error('Overflow: Varint exceeds 64 bits.');
}
} while ((byte & 0x80) !== 0);
return varint;
}
readString(): string {
const length = Number(this.readVarint());
const str: string = this._buffer.toString('utf8', this._offset, this._offset + length);
this._offset += length;
return str;
}
readBlob(size: number): Buffer {
const blob: Buffer = this._buffer.subarray(this._offset, this._offset + size);
this._offset += size;
return blob;
}
}

83
src/core/chacha8.ts Normal file
View file

@ -0,0 +1,83 @@
import { XorStream , createCipher } from './arx';
export function rotl(a: number, b: number): number {
return (a << b) | (a >>> (32 - b));
}
/**
* ChaCha core function.
*/
// prettier-ignore
function chachaCore(
s: Uint32Array, k: Uint32Array, n: Uint32Array, out: Uint32Array, cnt: number, rounds = 20,
): void {
const y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], // "expa" "nd 3" "2-by" "te k"
y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], // Key Key Key Key
y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], // Key Key Key Key
y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; // Counter Counter Nonce Nonce
// Save state to temporary variables
let x00 = y00, x01 = y01, x02 = y02, x03 = y03,
x04 = y04, x05 = y05, x06 = y06, x07 = y07,
x08 = y08, x09 = y09, x10 = y10, x11 = y11,
x12 = y12, x13 = y13, x14 = y14, x15 = y15;
for (let r = 0; r < rounds; r += 2) {
x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 16);
x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 12);
x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 8);
x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 7);
x01 = (x01 + x05) | 0; x13 = rotl(x13 ^ x01, 16);
x09 = (x09 + x13) | 0; x05 = rotl(x05 ^ x09, 12);
x01 = (x01 + x05) | 0; x13 = rotl(x13 ^ x01, 8);
x09 = (x09 + x13) | 0; x05 = rotl(x05 ^ x09, 7);
x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 16);
x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 12);
x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 8);
x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 7);
x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 16);
x11 = (x11 + x15) | 0; x07 = rotl(x07 ^ x11, 12);
x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 8);
x11 = (x11 + x15) | 0; x07 = rotl(x07 ^ x11, 7);
x00 = (x00 + x05) | 0; x15 = rotl(x15 ^ x00, 16);
x10 = (x10 + x15) | 0; x05 = rotl(x05 ^ x10, 12);
x00 = (x00 + x05) | 0; x15 = rotl(x15 ^ x00, 8);
x10 = (x10 + x15) | 0; x05 = rotl(x05 ^ x10, 7);
x01 = (x01 + x06) | 0; x12 = rotl(x12 ^ x01, 16);
x11 = (x11 + x12) | 0; x06 = rotl(x06 ^ x11, 12);
x01 = (x01 + x06) | 0; x12 = rotl(x12 ^ x01, 8);
x11 = (x11 + x12) | 0; x06 = rotl(x06 ^ x11, 7);
x02 = (x02 + x07) | 0; x13 = rotl(x13 ^ x02, 16);
x08 = (x08 + x13) | 0; x07 = rotl(x07 ^ x08, 12);
x02 = (x02 + x07) | 0; x13 = rotl(x13 ^ x02, 8);
x08 = (x08 + x13) | 0; x07 = rotl(x07 ^ x08, 7);
x03 = (x03 + x04) | 0; x14 = rotl(x14 ^ x03, 16);
x09 = (x09 + x14) | 0; x04 = rotl(x04 ^ x09, 12);
x03 = (x03 + x04) | 0; x14 = rotl(x14 ^ x03, 8);
x09 = (x09 + x14) | 0; x04 = rotl(x04 ^ x09, 7);
}
// Write output
let oi = 0;
out[oi++] = (y00 + x00) | 0; out[oi++] = (y01 + x01) | 0;
out[oi++] = (y02 + x02) | 0; out[oi++] = (y03 + x03) | 0;
out[oi++] = (y04 + x04) | 0; out[oi++] = (y05 + x05) | 0;
out[oi++] = (y06 + x06) | 0; out[oi++] = (y07 + x07) | 0;
out[oi++] = (y08 + x08) | 0; out[oi++] = (y09 + x09) | 0;
out[oi++] = (y10 + x10) | 0; out[oi++] = (y11 + x11) | 0;
out[oi++] = (y12 + x12) | 0; out[oi++] = (y13 + x13) | 0;
out[oi++] = (y14 + x14) | 0; out[oi++] = (y15 + x15) | 0;
}
/*
* Reduced 8-round chacha, described in original paper.
*/
export const chacha8: XorStream = createCipher(chachaCore, {
counterRight: false,
counterLength: 4,
rounds: 8,
});

30
src/core/crypto-data.ts Normal file
View file

@ -0,0 +1,30 @@
import BN from 'bn.js';
import { eddsa as EDDSA } from 'elliptic';
import RedBN from './interfaces';
export const ec: EDDSA = new EDDSA('ed25519');
const { red } = ec.curve;
export const A = new BN(486662, 10).toRed(red);
// sqrt(-1)
// https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto-ops-data.c#L12
//export const sqrtm1 = new BN(1).toRed(red).redNeg().redSqrt();
export const sqrtm1: RedBN = new BN('547cdb7fb03e20f4d4b2ff66c2042858d0bce7f952d01b873b11e4d8b5f15f3d', 'hex').toRed(red);
// sqrt(-2 * A * (A + 2))
// https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto-ops-data.c#L843
export const fffb1: RedBN = new BN('7e71fbefdad61b1720a9c53741fb19e3d19404a8b92a738d22a76975321c41ee', 'hex').toRed(red);
// sqrt(2 * A * (A + 2))
// https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto-ops-data.c#L844
export const fffb2: RedBN = new BN('32f9e1f5fba5d3096e2bae483fe9a041ae21fcb9fba908202d219b7c9f83650d', 'hex').toRed(red);
// sqrt(-sqrt(-1) * A * (A + 2))
// https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto-ops-data.c#L845
export const fffb3: RedBN = new BN('1a43f3031067dbf926c0f4887ef7432eee46fc08a13f4a49853d1903b6b39186', 'hex').toRed(red);
// sqrt(sqrt(-1) * A * (A + 2))
// https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto-ops-data.c#L846
export const fffb4: RedBN = new BN('674a110d14c208efb89546403f0da2ed4024ff4ea5964229581b7d8717302c66', 'hex').toRed(red);

504
src/core/crypto.ts Normal file
View file

@ -0,0 +1,504 @@
import { randomBytes } from 'crypto';
import BN from 'bn.js';
import { curve } from 'elliptic';
import * as sha3 from 'js-sha3';
import createKeccakHash from 'keccak';
import { chacha8 } from './chacha8';
import {
fffb4,
fffb3,
sqrtm1,
fffb2,
fffb1,
A,
ec,
} from './crypto-data';
import {
encodeInt,
decodePoint,
reduceScalar,
encodePoint,
decodeScalar,
squareRoot,
decodeInt,
} from './helpers';
import RedBN from './interfaces';
import { serializeVarUint } from './serialize';
import { SpendKeypair } from './types';
const ADDRESS_CHECKSUM_SIZE = 8;
export const SCALAR_1DIV8: Buffer = (() => {
const scalar: Buffer = Buffer.alloc(32);
scalar.writeBigUInt64LE(BigInt('0x6106e529e2dc2f79'), 0);
scalar.writeBigUInt64LE(BigInt('0x07d39db37d1cdad0'), 8);
scalar.writeBigUInt64LE(BigInt('0x0'), 16);
scalar.writeBigUInt64LE(BigInt('0x0600000000000000'), 24);
return scalar;
})();
export const HASH_SIZE = 32;
export function getChecksum(buffer: Buffer): string {
return sha3.keccak_256(buffer).substring(0, ADDRESS_CHECKSUM_SIZE);
}
export function getDerivationToScalar(txPubKey: string, secViewKey: string, outIndex: number): Buffer {
const txPubKeyBuf: Buffer = Buffer.from(txPubKey, 'hex');
const secViewKeyBuf: Buffer = Buffer.from(secViewKey, 'hex');
const sharedSecret: Buffer = generateKeyDerivation(txPubKeyBuf, secViewKeyBuf);
return derivationToScalar(sharedSecret, outIndex);
}
/*
* out.concealing_point = (crypto::hash_helper_t::hs(CRYPTO_HDS_OUT_CONCEALING_POINT, h) * crypto::point_t(apa.view_public_key)).to_public_key(); // Q = 1/8 * Hs(domain_sep, Hs(8 * r * V, i) ) * 8 * V
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/currency_core/currency_format_utils.cpp#L1270
*/
export function calculateConcealingPoint(Hs: Buffer, pubViewKeyBuff: Buffer): Buffer {
const scalar: BN = decodeScalar(Hs, 'Invalid sсalar');
const P: curve.edwards.EdwardsPoint = decodePoint(pubViewKeyBuff, 'Invalid public key');
const P2: curve.base.BasePoint = P.mul(scalar);
return encodePoint(P2);
}
/*
* out.blinded_asset_id = (crypto::c_scalar_1div8 * blinded_asset_id).to_public_key(); // T = 1/8 * (H_asset + s * X)
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/currency_core/currency_format_utils.cpp#L1278
*/
export function calculateBlindedAssetId(Hs: Buffer, assetId: Buffer, X: Buffer): Buffer {
const assetIdCopy: Buffer = Buffer.from(assetId);
const pointXCopy: Buffer = Buffer.from(X);
const hsScalar: BN = decodeScalar(Hs, 'Invalid sсalar');
const xP: curve.edwards.EdwardsPoint = decodePoint(pointXCopy, 'Invalid public key');
const sxP: curve.base.BasePoint = xP.mul(hsScalar);
const scalar1div8: BN = decodeScalar(SCALAR_1DIV8, 'Invalid sсalar');
const assetPoint: curve.edwards.EdwardsPoint = decodePoint(assetIdCopy, 'Invalid public key');
const pointT: curve.base.BasePoint = sxP.add(assetPoint);
const blindedAssetIdPoint: curve.base.BasePoint = pointT.mul(scalar1div8);
return encodePoint(blindedAssetIdPoint);
}
// todo: crypto::point_t asset_id = blinded_asset_id - asset_id_blinding_mask * crypto::c_point_X; // H = T - s * X
// https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/currency_core/currency_format_utils.cpp#L3289
/*
* generate_key_derivation
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L175
*/
export function generateKeyDerivation(txPubKey: Buffer, secKeyView: Buffer): Buffer {
const s: BN = decodeScalar(secKeyView, 'Invalid secret key');
const P: curve.edwards.EdwardsPoint = decodePoint(txPubKey, 'Invalid public key');
const P2: curve.base.BasePoint = P.mul(s);
// Multiplying the initial derivation by 8, adhering to specific cryptographic protocol requirements
const P3: curve.base.BasePoint = P2.mul(new BN('8'));
return encodePoint(P3);
}
/*
* derive_public_key
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L207
*/
export function derivePublicKey(
derivation: Buffer,
outIndex: number,
pubSpendKeyBuf: Buffer,
): Buffer {
const P1: curve.base.BasePoint = decodePoint(pubSpendKeyBuf, 'Invalid public key');
const scalar: Buffer = derivationToScalar(derivation, outIndex);
/*
* Scalar multiplication of the base point with the derived scalar to get the intermediary public key
* Hs(8 * r * V, i)G
*/
const P: curve.base.BasePoint = ec.curve.g.mul(decodeInt(scalar));
// Hs(8 * r * V, i)G + S
const P2: curve.base.BasePoint = P.add(P1);
return encodePoint(P2);
}
/*
* derive_secret_key
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L227
*/
export function deriveSecretKey(derivation: Buffer, outIndex: number, sec: Buffer): Buffer {
const s: BN = decodeScalar(sec, 'Invalid secret key');
const scalar: Buffer = derivationToScalar(derivation, outIndex);
const key: BN = s
.add(decodeInt(scalar))
.umod(ec.curve.n);
return encodeInt(key);
}
/*
* derivation_to_scalar
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L190
*/
export function derivationToScalar(derivation: Buffer, outIndex: number): Buffer {
const data: Buffer = Buffer.concat([
derivation,
serializeVarUint(outIndex),
]);
return hashToScalar(data);
}
export function fastHash(data: Buffer): Buffer {
const hash: Buffer = createKeccakHash('keccak256').update(data).digest();
return hash;
}
/*
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto-sugar.h#L1386
*/
export function hs(str32: Buffer, h: Buffer): Buffer {
const elements: Buffer[] = [str32, h];
const data: Buffer = Buffer.concat(elements);
return hashToScalar(data);
}
/*
* hash_to_scalar
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L115
*/
export function hashToScalar(data: Buffer): Buffer {
const hash: Buffer = fastHash(data);
return reduceScalar32(hash);
}
export function reduceScalar32(scalar: Buffer): Buffer {
const num: BN = decodeInt(scalar);
return encodeInt(num.umod(ec.curve.n));
}
/*
* generate_key_image
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L296
*/
export function calculateKeyImage(pub: Buffer, sec: Buffer): Buffer {
const s: BN = decodeScalar(sec, 'Invalid secret key');
const P1: curve.base.BasePoint = hashToEc(pub);
const P2: curve.base.BasePoint = P1.mul(s);
return encodePoint(P2);
}
/*
* hash_to_ec
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L286
*/
export function hashToEc(ephemeralPubKey: Buffer): curve.base.BasePoint {
const hash: Buffer = fastHash(ephemeralPubKey);
const P: curve.edwards.EdwardsPoint = hashToPoint(hash);
return P.mul(new BN(8).toRed(ec.curve.red));
}
/*
* ge_fromfe_frombytes_vartime
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto-ops.c#L2209
*/
export function hashToPoint(hash: Buffer): curve.edwards.EdwardsPoint {
const u: RedBN = decodeInt(hash).toRed(ec.curve.red);
// v = 2 * u^2
const v: RedBN = u.redMul(u).redMul(new BN(2).toRed(ec.curve.red));
// w = 2 * u^2 + 1 = v + 1
const w: RedBN = v.redAdd(new BN(1).toRed(ec.curve.red));
// t = w^2 - 2 * A^2 * u^2 = w^2 - A^2 * v
const t: RedBN = w.redMul(w).redSub(A.redMul(A).redMul(v));
// x = sqrt( w / w^2 - 2 * A^2 * u^2 ) = sqrt( w / t )
let x: RedBN = squareRoot(w, t);
let negative = false;
// check = w - x^2 * t
let check: RedBN = w.redSub(x.redMul(x).redMul(t));
if (!check.isZero()) {
// check = w + x^2 * t
check = w.redAdd(x.redMul(x).redMul(t));
if (!check.isZero()) {
negative = true;
} else {
// x = x * fe_fffb1
x = x.redMul(fffb1);
}
} else {
// x = x * fe_fffb2
x = x.redMul(fffb2);
}
let odd: boolean;
let r: RedBN;
if (!negative) {
odd = false;
// r = -2 * A * u^2 = -1 * A * v
r = A.redNeg().redMul(v);
// x = x * u
x = x.redMul(u);
} else {
odd = true;
// r = -1 * A
r = A.redNeg();
// check = w - sqrtm1 * x^2 * t
check = w.redSub(x.redMul(x).redMul(t).redMul(sqrtm1));
if (!check.isZero()) {
// check = w + sqrtm1 * x^2 * t
check = w.redAdd(x.redMul(x).redMul(t).redMul(sqrtm1));
if (!check.isZero()) {
throw new TypeError('Invalid point');
} else {
x = x.redMul(fffb3);
}
} else {
x = x.redMul(fffb4);
}
}
if (x.isOdd() !== odd) {
// x = -1 * x
x = x.redNeg();
}
// z = r + w
const z: RedBN = r.redAdd(w);
// y = r - w
const y: RedBN = r.redSub(w);
// x = x * z
x = x.redMul(z);
return ec.curve.point(x, y, z);
}
export function generateChaCha8Key(pass: Buffer): Buffer {
const hash: Buffer = fastHash(pass);
if (hash.length !== HASH_SIZE) {
throw new Error('Size of hash must be at least that of chacha8_key');
}
return hash;
}
export function chachaCrypt(paymentId: Buffer, derivation: Buffer): Buffer {
const key: Buffer = generateChaCha8Key(Buffer.from(derivation));
const iv: Uint8Array = new Uint8Array(Buffer.alloc(12).fill(0));
const decryptedBuff: Uint8Array = chacha8(key, iv, paymentId);
return Buffer.from(decryptedBuff);
}
/*
* keys_from_default
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L88
*/
export function keysFromDefault(aPart: Buffer, keysSeedBinarySize: number): SpendKeypair {
// aPart == 32 bytes
const tmp: Buffer = Buffer.alloc(64).fill(0);
if (!(tmp.length >= keysSeedBinarySize)) {
throw new Error('size mismatch');
}
tmp.set(aPart);
const hash: Buffer = fastHash(tmp.subarray(0, 32));
hash.copy(tmp, 32);
const scalar: BN = decodeInt(tmp);
const reducedScalarBuff: Buffer = Buffer.alloc(32);
const reducedScalar: BN = reduceScalar(scalar, ec.curve.n);
// for working in web building, without to Buffer
reducedScalarBuff.set(reducedScalar.toArrayLike(Buffer, 'le', 32));
const basePoint: curve.base.BasePoint = ec.curve.g;
const secretKey: Buffer = reducedScalarBuff.subarray(0, 32);
const s: BN = decodeScalar(secretKey);
const P2: curve.base.BasePoint = basePoint.mul(s);
return {
publicSpendKey: encodePoint(P2).toString('hex'),
secretSpendKey: Buffer.from(secretKey).toString('hex'),
};
}
/*
* generate_seed_keys
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L108
*/
export function generateSeedKeys(keysSeedBinarySize: number): SpendKeypair {
const keysSeedBinary: Buffer = getRandomBytes(keysSeedBinarySize);
const {
secretSpendKey,
publicSpendKey,
} = keysFromDefault(keysSeedBinary, keysSeedBinarySize);
return {
secretSpendKey,
publicSpendKey,
};
}
export function generateSeedKeysWeb(size: number): SpendKeypair {
const seed: Buffer = getRandomBytes(size);
const {
secretSpendKey,
publicSpendKey,
} = keysFromDefault(seed, size);
return {
seedKey: seed.toString('hex'),
secretSpendKey,
publicSpendKey,
};
}
export function getRandomBytes(numBytes: number): Buffer {
const array: Uint8Array = new Uint8Array(numBytes);
return typeof window !== 'undefined' && window.crypto?.getRandomValues(array)
? Buffer.from(array)
: randomBytes(numBytes);
}
/*
* dependent_key
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L129
*/
export function dependentKey(secretSpendKey: Buffer): string {
if (secretSpendKey.length !== 32) {
throw new Error('Invalid secret spend key');
}
const secretViewKey: Buffer = hashToScalar(secretSpendKey);
return secretViewKey.toString('hex');
}
/*
* secret_key_to_public_key
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L165
*/
export function secretKeyToPublicKey(secretViewKey: Buffer): string {
const s: BN = decodeScalar(secretViewKey, 'Invalid secret key');
const basePoint: curve.base.BasePoint = ec.curve.g;
const P2: curve.base.BasePoint = basePoint.mul(s);
return encodePoint(P2).toString('hex');
}
/*
* generate_signature
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L241
*/
export function generateSignature(message: Buffer, privateKey: Buffer, pubKey: Buffer): string {
const h: Buffer = fastHash(message);
const s: BN = decodeScalar(privateKey);
const publicKey: string = secretKeyToPublicKey(privateKey);
const pubKeyBuf: Buffer = Buffer.from(publicKey, 'hex');
if (!pubKeyBuf.equals(pubKey)) {
throw new RangeError('Incorrect public key');
}
while (true) {
const k: BN = decodeInt(getRandomScalar(randomBytes(32), 32));
const K: curve.base.BasePoint = ec.curve.g.mul(k);
const buf = {
h: h,
key: pubKeyBuf,
comm: encodePoint(K),
};
const bufForHash: Buffer = Buffer.concat([buf.h, buf.key, buf.comm]);
const hashFromBuffer: Buffer = hashToScalar(bufForHash);
const c: BN = decodeInt(hashFromBuffer);
if (c.isZero()) {
continue;
}
const r: BN = k
.sub(s.mul(c))
.umod(ec.curve.n);
if (r.isZero()) {
continue;
}
const encodedC: Buffer = c.toArrayLike(Buffer, 'le', 32);
const encodedR: Buffer = r.toArrayLike(Buffer, 'le', 32);
return encodedR.toString('hex') + encodedC.toString('hex');
}
}
/*
* check_signature
* https://github.com/hyle-team/zano/blob/2817090c8ac7639d6f697d00fc8bcba2b3681d90/src/crypto/crypto.cpp#L265
*/
export function checkSignature(
message: Buffer,
publicKey: Buffer,
signature: { r: Buffer; c: Buffer },
): boolean {
try {
const r: BN = decodeScalar(signature.r);
const c: BN = decodeScalar(signature.c);
const P: curve.edwards.EdwardsPoint = decodePoint(publicKey);
const h: Buffer = fastHash(message);
const B: curve.base.BasePoint = ec.curve.g;
const R: curve.base.BasePoint = P.mul(c).add(B.mul(r));
const bufComm: Buffer = encodePoint(R);
const buf = {
h,
key: publicKey,
comm: bufComm,
};
const bufForHash: Buffer = Buffer.concat([buf.h, buf.key, buf.comm]);
const hashFromBuffer: Buffer = hashToScalar(bufForHash);
const calculatedC: BN = decodeInt(hashFromBuffer);
return calculatedC.eq(c);
} catch (error) {
console.error('Error during signature verification:', error.message);
return false;
}
}
function getRandomScalar(aPart: Buffer, keysSeedBinarySize: number) {
// aPart == 32 bytes
const tmp: Buffer = Buffer.alloc(64).fill(0);
if (!(tmp.length >= keysSeedBinarySize)) {
throw new Error('size mismatch');
}
tmp.set(aPart);
const hash: Buffer = fastHash(tmp.subarray(0, 32));
hash.copy(tmp, 32);
const scalar: BN = decodeInt(tmp);
const reducedScalarBuff: Buffer = Buffer.alloc(32);
const reducedScalar: BN = reduceScalar(scalar, ec.curve.n);
// for working in web building, without to Buffer
reducedScalarBuff.set(reducedScalar.toArrayLike(Buffer, 'le', 32));
const secretKey: Buffer = reducedScalarBuff.subarray(0, 32);
return secretKey;
}

86
src/core/helpers.ts Normal file
View file

@ -0,0 +1,86 @@
import { Buffer } from 'buffer';
import BN from 'bn.js';
import { curve } from 'elliptic';
import { ec, sqrtm1 } from './crypto-data';
import RedBN from './interfaces';
/*
* Decode little-endian number
*/
export function decodeInt(buf: Buffer): BN {
if (typeof buf === 'string') {
buf = Buffer.from(buf, 'hex');
}
return new BN(buf, 'hex', 'le');
}
/*
* Square root candidate
* x = (u/v)^(p+3)/8 = u*v^3*(u*v^7)^(p-5)/8
* https://tools.ietf.org/html/rfc8032#section-5.1.3
* https://crypto.stackexchange.com/questions/88868/why-computation-of-uv3uv7p-5-8-is-suggested-instead-of-u-vp3-8
*/
export function squareRoot(u: RedBN, v: RedBN) {
return u.redMul(v.redPow(new BN(3)))
.redMul(u.redMul(v.redPow(new BN(7))).redPow(ec.curve.p.subn(5).divn(8)));
}
/*
* Decode little-endian number and veryfy < n
*/
export function decodeScalar(buf: Buffer, message = 'Invalid scalar'): BN {
const scalar: BN = decodeInt(buf);
if (scalar.gte(ec.curve.n)) {
throw new RangeError(message);
}
return scalar;
}
export function encodePoint(P: curve.base.BasePoint): Buffer {
return Buffer.from(ec.encodePoint(P));
}
export function reduceScalar(scalar: BN, curveOrder: BN): BN {
return scalar.mod(curveOrder);
}
export function decodePoint(buf: Buffer, message = 'Invalid point'): curve.edwards.EdwardsPoint {
// compress data if curve isOdd
const xIsOdd: boolean = (buf[buf.length - 1] & 0x80) !== 0;
buf[buf.length - 1] = buf[buf.length - 1] & ~0x80;
let y: RedBN = decodeInt(buf) as RedBN;
if (y.gte(ec.curve.p)) {
throw new RangeError(message);
}
y = y.toRed(ec.curve.red);
// x^2 = (y^2 - c^2) / (c^2 d y^2 - a) = u / v
const y2: RedBN = y.redSqr();
const u: RedBN = y2.redSub(ec.curve.c2 as RedBN);
const v: RedBN = y2.redMul(ec.curve.d as RedBN).redMul(ec.curve.c2 as RedBN).redSub(ec.curve.a as RedBN);
let x: RedBN = squareRoot(u, v);
if (!u.redSub(x.redSqr().redMul(v)).isZero()) {
x = x.redMul(sqrtm1);
if (!u.redSub(x.redSqr().redMul(v)).isZero()) {
throw new RangeError(message);
}
}
if (x.fromRed().isZero() && xIsOdd) {
throw new RangeError(message);
}
if (x.fromRed().isOdd() !== xIsOdd) {
x = x.redNeg();
}
return ec.curve.point(x, y);
}
export function encodeInt(num: BN) {
return num.toArrayLike(Buffer, 'le', 32);
}

78
src/core/interfaces.ts Normal file
View file

@ -0,0 +1,78 @@
import BN from 'bn.js';
/**
* BN operations in a reduction context.
*/
declare class RedBN extends BN {
/**
* Convert back a number using a reduction context
*/
fromRed(): BN;
/**
* modular addition
*/
redAdd(b: RedBN): RedBN;
/**
* in-place modular addition
*/
redIAdd(b: RedBN): RedBN;
/**
* modular subtraction
*/
redSub(b: RedBN): RedBN;
/**
* in-place modular subtraction
*/
redISub(b: RedBN): RedBN;
/**
* modular shift left
*/
redShl(num: number): RedBN;
/**
* modular multiplication
*/
redMul(b: RedBN): RedBN;
/**
* in-place modular multiplication
*/
redIMul(b: RedBN): RedBN;
/**
* modular square
*/
redSqr(): RedBN;
/**
* in-place modular square
*/
redISqr(): RedBN;
/**
* modular square root
*/
redSqrt(): RedBN;
/**
* modular inverse of the number
*/
redInvm(): RedBN;
/**
* modular negation
*/
redNeg(): RedBN;
/**
* modular exponentiation
*/
redPow(b: BN): RedBN;
}
export default RedBN;

25
src/core/serialize.ts Normal file
View file

@ -0,0 +1,25 @@
export function serializeVarUint(varuint: number | bigint): Buffer {
const type = typeof varuint;
if (
(type !== 'number' && type !== 'bigint') ||
(type === 'number' && !Number.isInteger(varuint))
) {
throw new Error(
'varuint value must be integer-like',
);
}
const bytes = [];
varuint = BigInt(varuint);
while (varuint >= 0x80n) {
const byte = Number((varuint & 0x7fn) | 0x80n);
bytes.push(byte);
varuint >>= 7n;
}
bytes.push(Number(varuint));
const buffer: Buffer = Buffer.from(bytes);
return buffer;
}

5
src/core/types.ts Normal file
View file

@ -0,0 +1,5 @@
export type SpendKeypair = {
seedKey?: string;
secretSpendKey: string;
publicSpendKey: string;
}

25
src/decode/constants.ts Normal file
View file

@ -0,0 +1,25 @@
export const TAG_LENGTH = 1;
export const FLAG_LENGTH = 1;
export const SPEND_KEY_LENGTH = 32;
export const VIEW_KEY_LENGTH = 32;
export const CHECKSUM_LENGTH = 4;
export const BUFFER_ADDRESS_LENGTH: number =
TAG_LENGTH +
FLAG_LENGTH +
SPEND_KEY_LENGTH +
VIEW_KEY_LENGTH +
CHECKSUM_LENGTH;
export const ADDRESS_REGEX = /^Z[a-zA-Z0-9]{96}$/;
export const INTEGRATED_ADDRESS_REGEX = /^iZ[a-zA-Z0-9]{106}$/;
export const PAYMENT_ID_LENGTH = 8;
export const INTEGRATED_ADDRESS_FLAG_PREFIX = 0x6c;
export const INTEGRATED_ADDRESS_TAG_PREFIX = 0xf8;
export const ADDRESS_FLAG_PREFIX = 0x01;
export const ADDRESS_TAG_PREFIX = 0xC5;
export const BUFFER_INTEGRATED_ADDRESS_LENGTH =
BUFFER_ADDRESS_LENGTH +
PAYMENT_ID_LENGTH;
export const PAYMENT_ID_REGEX = /^[a-fA-F0-9]{16}$/;
export const PUBLIC_KEY_REGEX = /^[0-9a-f]{64}$/i;
export const HEX_PUBKEY_REGEX = /^[a-fA-F0-9]{64}$/;
export const HEX_ENCRYPTED_PAYMENT_ID = /\b[a-fA-F0-9]{16}\b/;

View file

@ -0,0 +1,245 @@
import {
ADDRESS_REGEX,
BUFFER_ADDRESS_LENGTH,
BUFFER_INTEGRATED_ADDRESS_LENGTH,
CHECKSUM_LENGTH,
FLAG_LENGTH,
INTEGRATED_ADDRESS_REGEX,
PUBLIC_KEY_REGEX,
SPEND_KEY_LENGTH,
TAG_LENGTH,
VIEW_KEY_LENGTH,
} from './constants';
import {
DecodeVoutResult,
DecodeTransactionResult,
ZarcanumAddressKeys,
} from './types';
import { isTransactionObjectV3, satoshiToZano } from './utils/functions';
import { base58Decode } from '../core/base58';
import { getChecksum, secretKeyToPublicKey } from '../core/crypto';
import {
decodeAmount,
decryptPaymentId,
getConcealingPoint,
getNativeBlindedAsset,
getStealthAddress,
parseObjectInJson,
} from '../transaction/transaction-utils';
import {
AggregatedTxV3,
TransactionObject,
TransactionObjectV3,
TxOutZarcanum,
VoutEntry,
} from '../transaction/types/transactions';
export function decodeTransaction(
objectInJson: string,
secretViewKey: string,
addressOrPublicSpendKey: string,
): DecodeTransactionResult {
objectInJson = objectInJson.trim();
secretViewKey = secretViewKey.trim();
addressOrPublicSpendKey = addressOrPublicSpendKey.trim();
let addressKeys: ZarcanumAddressKeys;
if (ADDRESS_REGEX.test(addressOrPublicSpendKey)) {
addressKeys = getKeysFromAddress(addressOrPublicSpendKey);
} else if (PUBLIC_KEY_REGEX.test(addressOrPublicSpendKey)) {
const secretViewKeyBuf: Buffer = Buffer.from(secretViewKey, 'hex');
const publicViewKey: string = secretKeyToPublicKey(secretViewKeyBuf);
addressKeys = {
spendPublicKey: addressOrPublicSpendKey,
viewPublicKey: publicViewKey,
};
} else {
return { ok: false, error: 'Either address or valid publicSpendKey must be provided.' };
}
const tx: TransactionObject | TransactionObjectV3 | null = parseObjectInJson(objectInJson);
if (!tx) {
return { ok: false, error: 'Failed to parse transaction JSON.' };
}
if (!isTransactionObjectV3(tx)) {
return { ok: false, error: 'Only V3 transactions are supported.' };
}
const aggregated: AggregatedTxV3 = tx.AGGREGATED;
if (!aggregated.vin?.length || !aggregated.vout?.length) {
return { ok: false, error: 'Invalid V3 transaction: missing vin or vout.' };
}
const validPubKey: string | undefined = aggregated.extra?.find(
item => 'pub_key' in item && typeof item.pub_key === 'string' && item.pub_key.length > 0,
)?.pub_key;
if (!validPubKey) {
return { ok: false, error: 'Public key not found in V3 transaction.' };
}
const extractedPaymentId: string | undefined = extractPaymentId(tx);
const paymentId: string = decryptPaymentId(extractedPaymentId, validPubKey, secretViewKey);
let totalAmount = BigInt(0);
for (const [index, vout] of aggregated.vout.entries()) {
if (!validateVoutEntryDataV3(vout)) {
continue;
}
const result: DecodeVoutResult = decodeVoutEntryV3(
vout,
validPubKey,
index,
addressKeys,
secretViewKey,
);
if (!result.ok) {
continue;
}
totalAmount += result.amount;
}
return {
ok: true,
amount: satoshiToZano(totalAmount.toString()),
...(paymentId ? { paymentId } : { }),
};
}
function decodeVoutEntryV3(
vout: VoutEntry,
validPubKey: string,
index: number,
addressKeys: ZarcanumAddressKeys,
secretViewKey: string,
): DecodeVoutResult {
if (!vout || !vout.tx_out_zarcanum) {
return { ok: false, error: 'Missing tx_out_zarcanum in vout.' };
}
const stealthAddress: string = getStealthAddress(
validPubKey,
secretViewKey,
addressKeys.spendPublicKey,
index,
);
const concealingPoint: string = getConcealingPoint(
secretViewKey,
validPubKey,
addressKeys.viewPublicKey,
index,
);
const blindedAssetId: string = getNativeBlindedAsset(
secretViewKey,
validPubKey,
index,
);
const out: TxOutZarcanum = vout.tx_out_zarcanum;
if (
stealthAddress !== out.stealth_address ||
concealingPoint !== out.concealing_point ||
blindedAssetId !== out.blinded_asset_id
) {
return { ok: false, error: 'Output does not belong to this address (mismatch).' };
}
const decryptedAmount: bigint = decodeAmount(
secretViewKey,
validPubKey,
out.encrypted_amount,
index,
);
if (!decryptedAmount || typeof decryptedAmount !== 'bigint') {
return { ok: false, error: 'Failed to decrypt amount.' };
}
return { ok: true, amount: decryptedAmount };
}
function getKeysFromAddress(address: string): ZarcanumAddressKeys {
if (!ADDRESS_REGEX.test(address) && !INTEGRATED_ADDRESS_REGEX.test(address)) {
throw new Error('Invalid address format');
}
const buf: Buffer = base58Decode(address);
if (buf.length !== BUFFER_ADDRESS_LENGTH && buf.length !== BUFFER_INTEGRATED_ADDRESS_LENGTH) {
throw new Error('Invalid buffer address length');
}
const addressWithoutChecksum: Buffer = Buffer.from(buf.buffer, 0, buf.length - CHECKSUM_LENGTH);
const checksum: string = Buffer.from(buf.buffer, buf.length - CHECKSUM_LENGTH).toString('hex');
if (checksum !== getChecksum(addressWithoutChecksum)) {
throw new Error('Invalid address checksum');
}
const spendPublicKey: string = Buffer.from(
buf.buffer,
TAG_LENGTH + FLAG_LENGTH,
SPEND_KEY_LENGTH,
).toString('hex');
const viewPublicKey: string = Buffer.from(
buf.buffer,
TAG_LENGTH + FLAG_LENGTH + SPEND_KEY_LENGTH,
VIEW_KEY_LENGTH,
).toString('hex');
if (!spendPublicKey || spendPublicKey.length !== SPEND_KEY_LENGTH * 2 ||
!viewPublicKey || viewPublicKey.length !== VIEW_KEY_LENGTH * 2) {
throw new Error('Invalid key format in the address.');
}
return {
spendPublicKey,
viewPublicKey,
};
}
function validateVoutEntryDataV3(vout: VoutEntry): boolean {
if (!vout || !vout.tx_out_zarcanum) {
return false;
}
const {
stealth_address,
concealing_point,
amount_commitment,
blinded_asset_id,
encrypted_amount,
mix_attr,
} = vout.tx_out_zarcanum;
return (
typeof stealth_address === 'string' &&
typeof concealing_point === 'string' &&
typeof amount_commitment === 'string' &&
typeof blinded_asset_id === 'string' &&
typeof encrypted_amount === 'string' &&
typeof mix_attr === 'string'
);
}
function extractPaymentId(tx: TransactionObjectV3): string | null {
const body: string = tx?.attachment?.[0]?.attachment?.body;
if (typeof body !== 'string') {
return null;
}
const match: RegExpMatchArray = /\b[a-fA-F0-9]{16}\b/.exec(body);
return match ? match[0] : null;
}

11
src/decode/types.ts Normal file
View file

@ -0,0 +1,11 @@
export type ZarcanumAddressKeys = {
spendPublicKey: string;
viewPublicKey: string;
};
export type DecodeTransactionResult =
| { ok: true; amount: string; paymentId?: string }
| { ok: false; error: string }
export type DecodeVoutResult =
| { ok: true; amount: bigint }
| { ok: false; error: string }

View file

@ -0,0 +1,45 @@
import Big from 'big.js';
import type { TransactionObject, TransactionObjectV3 } from '../../transaction/types/transactions';
export function timestampMsToDate(timestampMs: number): Date {
return new Date(timestampMs * 1000);
}
export function satoshiToZano(satoshiAmount: string): string {
const satoshi: Big = new Big(satoshiAmount);
if (satoshi.lt(0)) {
throw new Error('The number of satoshi cannot be negative');
} else if (satoshi.eq(0)) {
return '0';
}
satoshi.e -= 12;
return satoshi.toFixed();
}
export function isTransactionObjectV3(tx: TransactionObject | TransactionObjectV3): tx is TransactionObjectV3 {
return (tx as TransactionObjectV3).AGGREGATED?.version === '3';
}
/**
* Safely parses a string to a number using Big.js.
* Returns null if the value is invalid or too large for a JS number.
*/
export function parseBigAmountToNumber(str: string): number | null {
try {
const big: Big = new Big(str);
const num = Number(big.toString());
if (!Number.isFinite(num)) {
console.error('Value too large for JS number:', str);
return null;
}
return num;
} catch (e) {
console.error('Invalid numeric string format:', str);
return null;
}
}

34
src/index.ts Normal file
View file

@ -0,0 +1,34 @@
export { decodeTransaction } from './decode/decode-service';
export type { DecodeTransactionResult } from './decode/types';
export {
generateAccount,
validateAccount,
generateAccountKeys,
privateKeyToPublicKey,
getAccountBySecretSpendKey,
getKeyPair,
} from './account/account-utils';
export type {
AccountResult,
AccountKeys,
KeyPair,
} from './account/types';
export {
getIntegratedAddress,
createIntegratedAddress,
getMasterAddress,
splitIntegratedAddress,
getKeysFromAddress,
generatePaymentId,
} from './address/address-utils';
export type {
SplitedIntegratedAddress,
ZarcanumAddressKeys,
} from './address/types';
export { mnemonicToSeed } from './mnemonic';
export type { MnemonicToSeedResult } from './mnemonic';
export { seedToMnemonic } from './mnemonic';
export type { SeedToMnemonicResult } from './mnemonic';

File diff suppressed because it is too large Load diff

4
src/mnemonic/index.ts Normal file
View file

@ -0,0 +1,4 @@
export { mnemonicToSeed } from './mnemonic-to-seed';
export { MnemonicToSeedResult } from './types';
export { seedToMnemonic } from './seed-to-mnemonic';
export { SeedToMnemonicResult } from './types';

View file

@ -0,0 +1,87 @@
import { phrases } from './consts/phrases';
import { MnemonicToSeedResult } from './types';
import { keysFromDefault } from '../core/crypto';
export const NUMWORDS = 1626;
const wordsMap: Map<string, number> = new Map(phrases.map(item => [item.phrase, item.value]));
const SEED_PHRASE_V1_WORDS_COUNT = 25;
const SEED_PHRASE_V2_WORDS_COUNT = 26;
const BINARY_SIZE_SEED = 32;
/**
* Converts a mnemonic seed phrase (25 or 26 words) into a secret spend key.
*
* The last one or two words of the phrase represent metadata:
* - If 25 words: the 25th word is the creation timestamp.
* - If 26 words: the 25th word is the timestamp, the 26th is a checksum + audit flag.
*
* @param seedPhraseRaw - Raw seed phrase string (mnemonic) containing 25 or 26 words.
* @returns The secret spend key as a hex string, or `false` if parsing failed.
*/
export function mnemonicToSeed(seedPhraseRaw: string): MnemonicToSeedResult {
const seedPhrase: string = seedPhraseRaw.trim();
const words: string[] = seedPhrase.split(/\s+/);
let keysSeedText: string;
let timestampWord: string;
if (words.length === SEED_PHRASE_V1_WORDS_COUNT) {
timestampWord = words.pop()!;
keysSeedText = words.join(' ');
} else if (words.length === SEED_PHRASE_V2_WORDS_COUNT) {
words.pop(); // drop audit+checksum
timestampWord = words.pop()!;
keysSeedText = words.join(' ');
} else {
console.error('Invalid seed phrase word count:', words.length);
return false;
}
let keysSeedBinary: Buffer;
try {
keysSeedBinary = text2binary(keysSeedText);
} catch (error) {
console.error('Failed to convert seed text to binary:', error);
return false;
}
if (!keysSeedBinary.length) {
console.error('Empty binary seed after conversion');
return false;
}
const { secretSpendKey } = keysFromDefault(keysSeedBinary, BINARY_SIZE_SEED);
return secretSpendKey;
}
function text2binary(text: string): Buffer {
const tokens: string[] = text.trim().split(/\s+/);
if (tokens.length % 3 !== 0) {
throw new Error('Invalid word count in mnemonic text');
}
const res: Buffer = Buffer.alloc((tokens.length / 3) * 4);
for (let i = 0; i < tokens.length / 3; i++) {
const w1: number = wordsMap.get(tokens[i * 3]);
const w2: number = wordsMap.get(tokens[i * 3 + 1]);
const w3: number = wordsMap.get(tokens[i * 3 + 2]);
if (w1 === undefined || w2 === undefined || w3 === undefined) {
throw new Error('Invalid word in mnemonic text');
}
const val: number = w1 + NUMWORDS * (((NUMWORDS - w1) + w2) % NUMWORDS) + NUMWORDS * NUMWORDS * (((NUMWORDS - w2) + w3) % NUMWORDS);
const byteIndex: number = i * 4;
res[byteIndex] = val & 0xFF;
res[byteIndex + 1] = (val >> 8) & 0xFF;
res[byteIndex + 2] = (val >> 16) & 0xFF;
res[byteIndex + 3] = (val >> 24) & 0xFF;
}
return res;
}

View file

@ -0,0 +1,100 @@
import { phrases } from './consts/phrases';
import { NUMWORDS } from './mnemonic-to-seed';
import type { SeedToMnemonicResult } from './types';
import { fastHash } from '../core/crypto';
export const wordsArray: string[] = phrases.map(p => p.phrase);
const WALLET_BRAIN_DATE_OFFSET = 1543622400;
const WALLET_BRAIN_DATE_QUANTUM = 604800;
const WALLET_BRAIN_DATE_MAX_WEEKS_COUNT = 800;
const CHECKSUM_MAX = NUMWORDS >> 1;
export function seedToMnemonic(keysSeedHex: string): SeedToMnemonicResult {
if (!keysSeedHex) {
throw new Error('Invalid seed hex');
}
const keysSeedBinary: Buffer = Buffer.from(keysSeedHex, 'hex');
const mnemonic: string = binaryToText(keysSeedBinary);
const timestamp: number = Math.floor(Date.now() / 1000);
const creationTimestampWord: string = getWordFromTimestamp(timestamp, false);
const timestampFromWord: number = getTimestampFromWord(creationTimestampWord, false);
const hashWithTimestamp: Buffer = Buffer.from(fastHash(keysSeedBinary));
hashWithTimestamp.writeBigUInt64LE(BigInt(timestampFromWord), 0);
const checksumHash: Buffer = fastHash(hashWithTimestamp);
const checksumValue: number = Number(checksumHash.readBigUInt64LE(0) % BigInt(CHECKSUM_MAX + 1)) || 0;
const auditableFlag = 0;
const checksumWord: string = wordByNum((checksumValue << 1) | (auditableFlag & 1));
return `${mnemonic} ${creationTimestampWord} ${checksumWord}`;
}
function wordByNum(index: number): string {
return phrases[index]?.phrase ?? '';
}
function numByWord(word: string): number {
const entry = phrases.find(p => p.phrase === word);
if (!entry) {
throw new Error(`Unable to find word "${word}" in mnemonic dictionary`);
}
return entry.value;
}
function binaryToText(binary: Buffer): string {
if (binary.length % 4 !== 0) {
throw new Error('Invalid binary data size for mnemonic encoding');
}
const words: string[] = [];
for (let i = 0; i < binary.length; i += 4) {
const val: number = binary.readUInt32LE(i);
const w1: number = val % NUMWORDS;
const w2: number = (Math.floor(val / NUMWORDS) + w1) % NUMWORDS;
const w3: number = (Math.floor(val / (NUMWORDS * NUMWORDS)) + w2) % NUMWORDS;
words.push(wordsArray[w1], wordsArray[w2], wordsArray[w3]);
}
return words.join(' ');
}
function getWordFromTimestamp(timestamp: number, usePassword: boolean): string {
const dateOffset: number = Math.max(timestamp - WALLET_BRAIN_DATE_OFFSET, 0);
let weeksCount = Math.trunc(dateOffset / WALLET_BRAIN_DATE_QUANTUM);
console.log(weeksCount);
if (weeksCount >= WALLET_BRAIN_DATE_MAX_WEEKS_COUNT) {
throw new Error('SEED PHRASE needs to be extended or refactored');
}
if (usePassword) {
weeksCount += WALLET_BRAIN_DATE_MAX_WEEKS_COUNT;
}
if (weeksCount > 0xffffffff) {
throw new Error(`Value too large for uint32: ${weeksCount}`);
}
return wordByNum(weeksCount);
}
export function getTimestampFromWord(word: string, passwordUsed: boolean): number {
let weeks = numByWord(word);
if (weeks >= WALLET_BRAIN_DATE_MAX_WEEKS_COUNT) {
weeks -= WALLET_BRAIN_DATE_MAX_WEEKS_COUNT;
passwordUsed = true;
} else {
passwordUsed = false;
}
return weeks * WALLET_BRAIN_DATE_QUANTUM + WALLET_BRAIN_DATE_OFFSET;
}

2
src/mnemonic/types.ts Normal file
View file

@ -0,0 +1,2 @@
export type MnemonicToSeedResult = string | false;
export type SeedToMnemonicResult = string;

View file

@ -0,0 +1,8 @@
export const CRYPTO_HDS_OUT_AMOUNT_MASK = Buffer.from('ZANO_HDS_OUT_AMOUNT_MASK_______\0', 'ascii');
export const CRYPTO_HDS_OUT_CONCEALING_POINT = Buffer.from('ZANO_HDS_OUT_CONCEALING_POINT__\0', 'ascii');
export const CRYPTO_HDS_OUT_ASSET_BLIND_MASK = Buffer.from('ZANO_HDS_OUT_ASSET_BLIND_MASK__\0', 'ascii');
export const POINT_X: Buffer = Buffer.from('3a25bcdb43f5d2c9dd063dc39a9e0987bafc6fcf2df1bc76322d75884a4a3820', 'hex');
export const NATIVE_ASSET_ID: Buffer = Buffer.from(
'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a',
'hex',
);

View file

@ -0,0 +1,152 @@
import {
NATIVE_ASSET_ID,
POINT_X,
CRYPTO_HDS_OUT_ASSET_BLIND_MASK,
CRYPTO_HDS_OUT_AMOUNT_MASK,
CRYPTO_HDS_OUT_CONCEALING_POINT,
} from './constants';
import { TransactionObject, TransactionObjectV3 } from './types/transactions';
import {
calculateKeyImage,
chachaCrypt,
deriveSecretKey,
calculateBlindedAssetId,
derivePublicKey,
generateKeyDerivation,
calculateConcealingPoint,
getDerivationToScalar,
hs,
} from '../core/crypto';
// Q = 1/8 * Hs(domain_sep, Hs(8 * r * V, i) ) * 8 * V
function getConcealingPoint(viewSecretKey: string, txPubKey: string, pubViewKey: string, outputIndex: number): string {
const h: Buffer = getDerivationToScalar(txPubKey, viewSecretKey, outputIndex); // Hs(8 * r * V, i)
const Hs: Buffer = hs(CRYPTO_HDS_OUT_CONCEALING_POINT, h); // Hs(domain_sep, Hs(8 * r * V, i) )
// point V equal pubViewKey
const pubViewKeyBuff: Buffer = Buffer.from(pubViewKey, 'hex');
const concealingPoint: Buffer = calculateConcealingPoint(Hs, pubViewKeyBuff);
return concealingPoint.toString('hex');
}
function decodeAmount(viewSecretKey: string, txPubKey: string, encryptedAmount: string | number, outputIndex: number): bigint {
const h: Buffer = getDerivationToScalar(txPubKey, viewSecretKey, outputIndex); // Hs(8 * r * V, i)
const Hs: Buffer = hs(CRYPTO_HDS_OUT_AMOUNT_MASK, h); // Hs(domain_sep, Hs(8 * r * V, i) )
const maskNumber = BigInt(Hs.readBigUInt64LE(0));
return BigInt(encryptedAmount) ^ maskNumber;
}
/*
* Generates a stealth address for a given transaction using the Dual-key Stealth Address Protocol.
*
* This function computes a one-time destination key by combining sender's public transaction key with recipient's view and spend keys.
* The formula for this stealth address (P_i) is given as P_i = H_s(rV, i)G + S, where:
* - r is the random scalar multiplication of the transaction public key
* - V is the recipient's public view key
* - i is the output index in the transaction
* - S is the recipient's public spend key
* - H_s(rV, i) is the hash of the product of r and V and the output index i
* - G is the base point of the Ed25519 curve
*
* @returns {string} The hexadecimal string representation of the ephemeral public key which serves as the one-time stealth address.
*/
function getStealthAddress(txPubKey: string, secViewKey: string, pubSpendKey: string, outIndex: number): string {
// R = rG
const txPubKeyBuf: Buffer = Buffer.from(txPubKey, 'hex');
// v = secret view key
const secViewKeyBuf: Buffer = Buffer.from(secViewKey, 'hex');
// S = public spend key
const pubSpendKeyBuf: Buffer = Buffer.from(pubSpendKey, 'hex');
/*
* Generates a key derivation by performing scalar multiplication using a transaction public key
* and a secret view key, then multiplies the result by 8.
*/
const derivation: Buffer = generateKeyDerivation(txPubKeyBuf, secViewKeyBuf);
// P_i = H_s(rV, i)G + S
/*
* Derives a public key by using a scalar multiplication of a derivation buffer and the hash of a base point (G),
* then adds a provided public spend key to the result.
*/
const stealth: Buffer = derivePublicKey(derivation, outIndex, pubSpendKeyBuf);
return stealth.toString('hex');
}
// crypto::point_t(de.asset_id) + asset_blinding_mask * crypto::c_point_X;
// H = T + s * X
/*
* Calculate blindedAsset based on native asset id, pointX, blindedMask.
* We will be able to check if the output is native or not
*/
function getNativeBlindedAsset(viewSecretKey: string, txPubKey: string, outputIndex: number): string {
const h: Buffer = getDerivationToScalar(txPubKey, viewSecretKey, outputIndex); // h = Hs(8 * r * V, i)
const s: Buffer = hs(CRYPTO_HDS_OUT_ASSET_BLIND_MASK, h); // Hs(domain_sep, Hs(8 * r * V, i) )
const blindedAssetId: Buffer = calculateBlindedAssetId(s, NATIVE_ASSET_ID, POINT_X);
return blindedAssetId.toString('hex');
}
function generateKeyImage(txPubKey: string, secViewKey: string, pubSpendKey: string, outIndex: number, spendSecretKey: string): string {
const txPubKeyBuf: Buffer = Buffer.from(txPubKey, 'hex');
const secViewKeyBuf: Buffer = Buffer.from(secViewKey, 'hex');
const pubSpendKeyBuf: Buffer = Buffer.from(pubSpendKey, 'hex');
const secSpendKeyBuf: Buffer = Buffer.from(spendSecretKey, 'hex');
/*
* Generates a key derivation by performing scalar multiplication using a transaction public key
* and a secret view key, then multiplies the result by 8.
*/
const derivation: Buffer = generateKeyDerivation(txPubKeyBuf, secViewKeyBuf);
// P_i = H_s(rV, i)G + S
/*
* Derives a public key by using a scalar multiplication of a derivation buffer and the hash of a base point (G),
* then adds a provided public spend key to the result.
*/
const secret: Buffer = deriveSecretKey(derivation, outIndex, secSpendKeyBuf);
const stealthAddress: Buffer = derivePublicKey(derivation, outIndex, pubSpendKeyBuf);
const keyImage: Buffer = calculateKeyImage(stealthAddress, secret);
return keyImage.toString('hex');
}
function decryptPaymentId(encryptedPaymentId: string, txPubKey: string, secViewKey: string): string {
const encryptedPaymentIdBuf: Buffer = Buffer.from(encryptedPaymentId, 'hex');
const txPubKeyBuff: Buffer = Buffer.from(txPubKey, 'hex');
const secViewKeyBuff: Buffer = Buffer.from(secViewKey, 'hex');
const derivation: Buffer = generateKeyDerivation(txPubKeyBuff, secViewKeyBuff);
const encrypted: Buffer = chachaCrypt(encryptedPaymentIdBuf, derivation);
return encrypted.toString('hex');
}
function parseObjectInJson(objectInJson: string): TransactionObject | TransactionObjectV3 | null {
try {
const decodedData: string = Buffer.from(objectInJson || '', 'base64').toString();
const txJson: string = prepareJson(decodedData);
return JSON.parse(txJson);
} catch (error) {
console.error('Error parse txJson:', error.message);
return null;
}
}
function prepareJson(decodedData: string): string {
return decodedData
.replace(/: ,/g, ': null,')
.replace(/: \d+"([^"]*)"/g, (match: string, str: string) => `: "${str}"`)
.replace(/: (\d+)/g, ': "$1"');
}
export {
getConcealingPoint,
decodeAmount,
getStealthAddress,
getNativeBlindedAsset,
generateKeyImage,
decryptPaymentId,
parseObjectInJson,
};

View file

@ -0,0 +1,2 @@
export * from './v2';
export * from './v3';

View file

@ -0,0 +1,60 @@
export type TransactionObject = {
version: 2;
vin: VinData[];
extra: ExtraData[];
vout: VoutData[];
attachment: AttachmentData[];
};
type TxinZcInputData = {
key_offsets: KeyOffsetData;
k_image: string;
etc_details: unknown[];
};
type KeyOffsetData = {
uint64_t: number;
};
type VinData = {
txin_zc_input: TxinZcInputData;
};
type AccountAddressData = {
spend_public_key: string;
view_public_key: string;
flags: number;
};
type ExtraData = {
receiver2: null;
acc_addr: AccountAddressData;
pub_key: string;
etc_tx_flags16: null;
v: number;
checksum: null;
encrypted_key_derivation: string;
derivation_hash: number;
derivation_hint: null;
msg: string;
zarcanum_tx_data_v1: { fee: number };
};
type TxOutData = {
stealth_address: string;
concealing_point: string;
amount_commitment: string;
blinded_asset_id: string;
encrypted_amount: number;
mix_attr: number;
};
type VoutData = {
tx_out_zarcanum: TxOutData;
};
type AttachmentData = {
flags: string;
service_id: string;
body?: string;
};

View file

@ -0,0 +1,101 @@
export type TransactionObjectV3 = {
AGGREGATED: AggregatedTxV3;
attachment: AttachmentEntry[];
signatures: SignatureEntry[];
};
export type AggregatedTxV3 = {
version: '3';
vin: VinDataV3[];
extra: ExtraEntry[];
vout: VoutEntry[];
hardfork_id: string;
};
export type VinDataV3 = {
txin_zc_input: TxinZcInputDataV3;
};
export type TxinZcInputDataV3 = {
key_offsets: KeyOffsetData[];
k_image: string;
etc_details: unknown[];
};
export type KeyOffsetData = {
uint64_t: string;
};
export type ExtraEntry = {
receiver2?: Receiver2;
pub_key?: string;
etc_tx_flags16?: {
v: string;
};
checksum?: Checksum;
derivation_hint?: {
msg: string;
};
zarcanum_tx_data_v1?: {
fee: string;
};
extra_attach_info?: ExtraAttachInfo;
};
export type Receiver2 = {
acc_addr: {
spend_public_key: string;
view_public_key: string;
flags: string;
};
};
export type Checksum = {
encrypted_key_derivation: string;
derivation_hash: string;
};
export type ExtraAttachInfo = {
sz: string;
hsh: string;
cnt: string;
};
export type VoutEntry = {
tx_out_zarcanum: TxOutZarcanum;
};
export type TxOutZarcanum = {
stealth_address: string;
concealing_point: string;
amount_commitment: string;
blinded_asset_id: string;
encrypted_amount: string;
mix_attr: string;
};
export type AttachmentEntry = {
attachment: {
service_id: string;
instruction: string;
body: string;
security: unknown[];
flags: string;
};
};
export type SignatureEntry = {
ZC_sig: ZcSignature;
};
export type ZcSignature = {
pseudo_out_amount_commitment: string;
pseudo_out_blinded_asset_id: string;
clsags_ggx: ClsagsGgx;
};
export type ClsagsGgx = {
c: string;
'(std::vector<scalar_t>&)(r_g)': string[];
'(std::vector<scalar_t>&)(r_x)': string[];
};

130
tests/account-utils.spec.ts Normal file
View file

@ -0,0 +1,130 @@
import { validateAccount, generateAccount, privateKeyToPublicKey, AccountKeys, getAccountBySecretSpendKey } from '../src';
import { ADDRESS_REGEX } from '../src/address/constants';
import * as crypto from '../src/core/crypto';
describe('generate account', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('creates unpredictable address', () => {
expect(generateAccount()).toMatchObject({
address: expect.stringMatching(ADDRESS_REGEX),
secretSpendKey: expect.stringMatching(/^([0-9a-fA-F]{2})+$/),
publicSpendKey: expect.stringMatching(/^([0-9a-fA-F]{2})+$/),
publicViewKey: expect.stringMatching(/^([0-9a-fA-F]{2})+$/),
secretViewKey: expect.stringMatching(/^([0-9a-fA-F]{2})+$/),
});
});
it('generate known account', () => {
jest.spyOn(crypto, 'generateSeedKeys').mockReturnValueOnce({
secretSpendKey: '6c225665aadb81ebce41bd94cbc78250aaf62f2636819b1cdcf47d4cbcd2b00d',
publicSpendKey: '0c27ece0fb489b344915d12745a89f9b6cb307c384286be12ae9311942aa89db',
});
expect(generateAccount()).toMatchObject({
address: 'ZxBpGz8qdG3SxgBYFKJqxjThPZpjqW1ouK3ZsyZnZCUqQ4Ndc9PiqkdJuEicMXPPSW5JidxK5bye7UYc1hkTHhxc1w4temC2A',
secretSpendKey: '6c225665aadb81ebce41bd94cbc78250aaf62f2636819b1cdcf47d4cbcd2b00d',
publicSpendKey: '0c27ece0fb489b344915d12745a89f9b6cb307c384286be12ae9311942aa89db',
});
});
});
describe('address validation', () => {
const address = 'ZxDFpn4k7xVYyc9VZ3LphrJbkpc46xfREace5bme1aXiMzKPAHA8jsTWcHSXhv9AdodSaoGXK9Mg7bk3ec4FkQrj357fZPWZX';
const secretSpendKey = '80b3e96a3eb765332b0fd3e44e0fefa58747a70025bf91aa4a7b758ab6f5590d';
const publicSpendKey = 'b3eee2376f32bf2bfb5cf9c023f569380c84ac8c64ddc8f7c109730dc8e97d7a';
const secretViewKey = '3e75ffee51eb21b1d6404ddcab5b3aaa49edbfe225e9a893d87074aacae46b09';
const publicViewKey = 'fa9c2811c53eb1044490e931f92ad9ddf317220df08ccfb5b83eccfdbd38f135';
const secretSpendKey2 = '9ed57f071db00695b18ea396d0f85ce18178b35643c038f09255edc326c4a502';
const publicSpendKey2 = 'd651f305d40bcbe27ced0ef48253623ec31da3a28130d08ddf6686179e418ff4';
const publicViewKey2 = '2b3e2bac27a3992b3f93285b1d08476a5723afdf3aa6961770ad7e7544325831';
it('created address should be valid', () => {
expect(validateAccount(address, publicSpendKey, publicViewKey, secretSpendKey, secretViewKey))
.toBe(true);
});
it('should throw on empty input', () => {
expect(() => validateAccount('', '', '', '', '')).toThrow('invalid address format');
});
it('should throw on invalid address keys', () => {
expect(() => validateAccount(address, '', '', '', '')).toThrow('invalid address keys');
});
it('should throw on invalid dependent secret view key', () => {
expect(() =>
validateAccount(address, publicSpendKey, publicViewKey, secretSpendKey2, secretViewKey)
).toThrow('invalid depend secret view key');
});
it('should throw on mismatched public spend key', () => {
expect(() =>
validateAccount(address, publicSpendKey2, publicViewKey, secretSpendKey, secretViewKey)
).toThrow('invalid address keys');
});
it('should throw on missing secret view key', () => {
expect(() =>
validateAccount(address, publicSpendKey2, publicViewKey, secretSpendKey, '')
).toThrow('invalid address keys');
});
it('should throw on empty dependent secret view key', () => {
expect(() =>
validateAccount(address, publicSpendKey, publicViewKey, secretSpendKey, '')
).toThrow('invalid depend secret view key');
});
it('should throw on mismatched public view key', () => {
expect(() =>
validateAccount(address, publicSpendKey, publicViewKey2, secretSpendKey, secretViewKey)
).toThrow('pub view key from secret key no equal provided pub view key');
});
});
describe('private key to public key', () => {
const secretSpendKey = '80b3e96a3eb765332b0fd3e44e0fefa58747a70025bf91aa4a7b758ab6f5590d';
const publicSpendKey = 'b3eee2376f32bf2bfb5cf9c023f569380c84ac8c64ddc8f7c109730dc8e97d7a';
it('should derive the correct public key from a given secret key', () => {
const actualPublicKey: string = privateKeyToPublicKey(secretSpendKey);
expect(actualPublicKey).toBe(publicSpendKey);
});
it('should throw an error for an invalid secret key (non-hex string)', () => {
const invalidKey = 'zzzz-not-a-hex-key';
expect(() => privateKeyToPublicKey(invalidKey)).toThrow('Invalid secret spend key');
});
it('should throw an error for a too short key', () => {
const shortKey = 'deadbeef';
expect(() => privateKeyToPublicKey(shortKey)).toThrow('Invalid secret spend key');
});
it('should throw an error for an empty string', () => {
expect(() => privateKeyToPublicKey('')).toThrow('Invalid secret spend key');
});
});
describe('get account by secret spend key', () => {
it('created account should be equal mock', () => {
const secretSpendKeyMock = '80b3e96a3eb765332b0fd3e44e0fefa58747a70025bf91aa4a7b758ab6f5590d';
const publicSpendKeyMock = 'b3eee2376f32bf2bfb5cf9c023f569380c84ac8c64ddc8f7c109730dc8e97d7a';
const secretViewKeyMock = '3e75ffee51eb21b1d6404ddcab5b3aaa49edbfe225e9a893d87074aacae46b09';
const publicViewKeyMock = 'fa9c2811c53eb1044490e931f92ad9ddf317220df08ccfb5b83eccfdbd38f135';
const accountKeys: AccountKeys = getAccountBySecretSpendKey(secretSpendKeyMock);
const { secretSpendKey, secretViewKey, publicSpendKey, publicViewKey } = accountKeys;
expect({ secretSpendKey, secretViewKey, publicSpendKey, publicViewKey }).toEqual({
secretSpendKey: secretSpendKeyMock,
secretViewKey: secretViewKeyMock,
publicSpendKey: publicSpendKeyMock,
publicViewKey: publicViewKeyMock
});
});
});

145
tests/address-utils.spec.ts Normal file
View file

@ -0,0 +1,145 @@
import { splitIntegratedAddress, getIntegratedAddress, getKeysFromAddress, encodeAddress } from '../src/address/address-utils';
import { base58Decode } from '../src/core/base58';
describe(
'testing the correctness of the address encoding function encodeAddress',
() => {
const tag = 197;
const flag = 1;
const spendPublicKey = '9f5e1fa93630d4b281b18bb67a3db79e9622fc703cc3ad4a453a82e0a36d51fa';
const viewPublicKey = 'a3f208c8f9ba49bab28eed62b35b0f6be0a297bcd85c2faa1eb1820527bcf7e3';
const address = encodeAddress(tag, flag, spendPublicKey, viewPublicKey);
it('checking the correctness of the result', () => {
expect(address)
.toBe('ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH');
});
it('checking the correctness of the address length', () => {
expect(address).toHaveLength(97);
});
it('should throw an error for invalid tag', () => {
expect(() => {
encodeAddress(-197, 1, '...', '...');
}).toThrow('Invalid tag');
});
it('should throw an error for invalid flag', () => {
expect(() => {
encodeAddress(197, -1, '...', '...');
}).toThrow('Invalid flag');
});
it('should throw an error for invalid public key', () => {
expect(() => {
encodeAddress(197, 1, 'invalid', viewPublicKey);
}).toThrow('Invalid spendPublicKey: must be a hexadecimal string with a length of 64');
});
it('should throw an error for invalid private key', () => {
expect(() => {
encodeAddress(197, 1, spendPublicKey, 'invalid');
}).toThrow('Invalid viewPrivateKey: must be a hexadecimal string with a length of 64');
});
},
);
describe(
'testing the correctness of the address decoding function getKeysFromZanoAddress',
() => {
const address = 'ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH';
const spendPublicKey = '9f5e1fa93630d4b281b18bb67a3db79e9622fc703cc3ad4a453a82e0a36d51fa';
const viewPublicKey = 'a3f208c8f9ba49bab28eed62b35b0f6be0a297bcd85c2faa1eb1820527bcf7e3';
it('checking the correctness of the address decoding', () => {
expect((getKeysFromAddress(address))).toStrictEqual({
spendPublicKey,
viewPublicKey,
});
});
it('should throw an error for invalid address format', () => {
expect(() => {
getKeysFromAddress('invalid');
}).toThrow('Invalid address format');
});
it('should throw an invalid character in base58 string', () => {
const invalidAddress = 'ZxD5aoLDPTdcaRx4uOpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH';
expect(() => {
getKeysFromAddress(invalidAddress);
}).toThrow('base58 string block contains invalid character');
});
it('should throw an invalid base58 string size', () => {
const invalidAddress = 'Z';
expect(() => {
base58Decode(invalidAddress);
}).toThrow('base58 string has an invalid size');
});
it('should throw an invalid address checksum', () => {
expect(() => {
(getKeysFromAddress('Zx' + '1'.repeat(95)));
}).toThrow('Invalid address checksum');
});
},
);
describe('getIntegratedAddress', () => {
const SUFFIX_LENGTH = 18; // paymentId + checksum
// Define test data
const integratedAddress = 'iZ2kFmwxRHoaRxm1ni8HnfUTkYuKbni8s4CE2Z4GgFfH99BJ6cnbAtJTgUnZjPj9CTCTKy1qqM9wPCTp92uBC7e47JPoHxGL5UU2D1tpQMg4';
const masterAddress = 'ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH';
const masterAddress2 = 'ZxDG8UrQMEVaRxm1ni8HnfUTkYuKbni8s4CE2Z4GgFfH99BJ6cnbAtJTgUnZjPj9CTCTKy1qqM9wPCTp92uBC7e41KkqnWH8F';
const masterBasedIntegratedAddress = 'iZ2Zi6RmTWwcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp3iTqEsjvJoco1aLSZXS6T';
const master2BasedIntegratedAddress = 'iZ2kFmwxRHoaRxm1ni8HnfUTkYuKbni8s4CE2Z4GgFfH99BJ6cnbAtJTgUnZjPj9CTCTKy1qqM9wPCTp92uBC7e47JQQbd6iYGx1S6AdHpq6';
// Compute desired outcomes for the slice operation
const integratedAddressWithoutSuffix: string = integratedAddress.slice(0, -SUFFIX_LENGTH);
const masterBasedIntegratedAddressWithoutSuffix: string = masterBasedIntegratedAddress.slice(0, -SUFFIX_LENGTH);
const master2BasedIntegratedAddressWithoutSuffix: string = master2BasedIntegratedAddress.slice(0, -SUFFIX_LENGTH);
// Addresses returned by ZanoAddressUtils
const addressFromIntegrated: string = getIntegratedAddress(integratedAddress);
const addressFromMaster: string = getIntegratedAddress(masterAddress);
const addressFromMaster2: string = getIntegratedAddress(masterAddress2);
it('ensures that truncating the last 18 characters from the integrated address is correct', () => {
expect(addressFromIntegrated.slice(0, -SUFFIX_LENGTH)).toBe(integratedAddressWithoutSuffix);
});
it('ensures that truncating the last 18 characters from the master-based integrated address is correct', () => {
expect(addressFromMaster.slice(0, -SUFFIX_LENGTH)).toBe(masterBasedIntegratedAddressWithoutSuffix);
});
it('ensures that truncating the last 18 characters from the second master-based integrated address is correct', () => {
expect(addressFromMaster2.slice(0, -SUFFIX_LENGTH)).toBe(master2BasedIntegratedAddressWithoutSuffix);
});
});
describe(
'testing the correctness of the address decoding in function splitIntegratedAddress',
() => {
const integratedAddress = 'iZ2kFmwxRHoaRxm1ni8HnfUTkYuKbni8s4CE2Z4GgFfH99BJ6cnbAtJTgUnZjPj9CTCTKy1qqM9wPCTp92uBC7e47JPoHxGL5UU2D1tpQMg4';
const masterAddress = 'ZxDG8UrQMEVaRxm1ni8HnfUTkYuKbni8s4CE2Z4GgFfH99BJ6cnbAtJTgUnZjPj9CTCTKy1qqM9wPCTp92uBC7e41KkqnWH8F';
const paymentId = '1e4cbed444118c99';
const invalidintegratedAddress = 'i03McELC3jGTgUnZjPj9CTCTKy1qqM9wPCTp92uBC7e47JR67Qv6wMFaRxm1ni8HnfUTkYuKbni8s4CE2Z4GgFfH999Pvhkaga42D1npn1Vc';
it('checking the correctness of the integrated address decoding', () => {
expect(splitIntegratedAddress(integratedAddress)).toStrictEqual({
masterAddress,
paymentId,
});
});
it('should throw an invalid format of the integreted address', () => {
expect(() => {
splitIntegratedAddress(invalidintegratedAddress);
}).toThrow('Invalid integratedAddress: must be a hexadecimal string with a length of 106 whit correct regex');
});
},
);

118
tests/address.ts Normal file
View file

@ -0,0 +1,118 @@
import { encodeAddress, getKeysFromAddress } from '../src/address/address-utils';
import { base58Decode, base58Encode } from '../src/core/base58';
import type { ZarcanumAddressKeys } from '../src/address/types';
const rawAddressBufferHex = 'c5 01 9f 5e 1f a9 36 30 d4 b2 81 b1 8b b6 7a 3d b7 9e 96 22 fc 70 3c c3 ad 4a 45 3a 82 e0 a3 6d 51 fa a3 f2 08 c8 f9 ba 49 ba b2 8e ed 62 b3 5b 0f 6b e0 a2 97 bc d8 5c 2f aa 1e b1 82 05 27 bc f7 e3 e2 38 1c d6';
function dataToEncodeFn(hexString: string): string {
return hexString.split(' ')
.map(byte => `\\x${byte}`)
.join('');
}
function encodeToHex(hexString: string): string {
return hexString
.match(/.{1,2}/g)
.map(byte => byte)
.join('');
}
export function bufferToHex(buffer: Buffer): string {
let combinedHexString = '';
for (let i = 0; i < buffer.length; i++) {
combinedHexString += buffer[i].toString(16).padStart(2, '0');
if (i < buffer.length - 1) {
combinedHexString += ' ';
}
}
return combinedHexString;
}
export function makeBytes(data: string): Uint8Array {
const byteArray: number[] = data.split('\\x').slice(1).map(byte => parseInt(byte, 16));
return new Uint8Array(byteArray);
}
function testEncode(expected: string, data: string) {
const byteData: Uint8Array = makeBytes(data);
const result: string = base58Encode(byteData);
console.log(`Expected: '${expected}', Received: '${result}', ByteData:`, byteData);
console.assert(result === expected, `Expected ${expected} but got ${result}`);
}
function runTests() {
testEncode('11', '\\x00');
testEncode('111', '\\x00\\x00');
testEncode('11111', '\\x00\\x00\\x00');
testEncode('111111', '\\x00\\x00\\x00\\x00');
testEncode('1111111', '\\x00\\x00\\x00\\x00\\x00');
testEncode('111111111', '\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('1111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('11111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('1111111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('11111111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('1111111111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('11111111111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('111111111111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('11111111111111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('111111111111111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('1111111111111111111111', '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00');
testEncode('22222222222VtB5VXc', '\\x06\\x15\\x60\\x13\\x76\\x28\\x79\\xF7\\xFF\\xFF\\xFF\\xFF\\xFF');
const expectedAddress = 'ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH';
const dataToAddress: string = dataToEncodeFn(rawAddressBufferHex);
testEncode(expectedAddress, dataToAddress);
}
function runTestEncodeAddress(address: string, viewPubKey: string, spendPubKey: string): void {
const addressBufferHex: string = dataToEncodeFn(bufferToHex(base58Decode(address)));
testEncode(
'ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH',
addressBufferHex,
);
const serializedViewKey: string = encodeToHex(Buffer.from(viewPubKey, 'hex').toString('hex'));
if(serializedViewKey !== viewPubKey) {
throw new Error('PubViewKey not matched.');
}
const serializedSpendKey: string = encodeToHex(Buffer.from(spendPubKey, 'hex').toString('hex'));
if(serializedSpendKey !== spendPubKey) {
throw new Error('PubSpendKey not matched.');
}
const encodedAddress: string = encodeAddress(197, 1, spendPubKey, viewPubKey);
if(encodedAddress !== address) {
throw new Error(`Encoded address not matched. Received ${encodedAddress}, Expected: ${address}`);
}
}
function runTestGetZanoKeys(address: string, viewPubKey: string, spendPubKey: string): void {
const keysFromAddress: ZarcanumAddressKeys = getKeysFromAddress(address);
if(keysFromAddress.spendPublicKey !== spendPubKey) {
throw new Error('spendPubKey not matched.');
}
if(keysFromAddress.viewPublicKey !== viewPubKey) {
throw new Error('viewPubKey not matched.');
}
}
void (async (): Promise<void> => {
runTests();
runTestEncodeAddress(
'ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH',
'a3f208c8f9ba49bab28eed62b35b0f6be0a297bcd85c2faa1eb1820527bcf7e3',
'9f5e1fa93630d4b281b18bb67a3db79e9622fc703cc3ad4a453a82e0a36d51fa',
);
runTestGetZanoKeys(
'ZxD5aoLDPTdcaRx4uCpyW4XiLfEXejepAVz8cSY2fwHNEiJNu6NmpBBDLGTJzCsUvn3acCVDVDPMV8yQXdPooAp338Se7AxeH',
'a3f208c8f9ba49bab28eed62b35b0f6be0a297bcd85c2faa1eb1820527bcf7e3',
'9f5e1fa93630d4b281b18bb67a3db79e9622fc703cc3ad4a453a82e0a36d51fa',
);
})();

View file

@ -0,0 +1,52 @@
import { DecodeTransactionResult } from '../src/decode/types';
import { objectInJson } from './fixtures';
import { decodeTransaction } from '../src/decode/decode-service';
import { createIntegratedAddress } from '../src/address/address-utils';
describe(
'Should be decode transaction and return amount and paymentId',
() => {
const secretViewKey = '81ef6415b815f675991585ebba71c8c4663a08893fd93ee149c48e797a2fdf09';
const publicSpendKey = '6be99667faa6b693fbcd808f94b8243540198c9cbb0b564f267f885d227804a2';
const integratedAddress = 'iZ285wzfsbjXYEgZVuDeFy74GFtYDFbvrFSJpL6TJQ3Z1mxZaKQLZsJHyVuCH4pQSr3rQunoH7cE4bjJSWmJqWSnKu6rPqXZ8HB1VxSHXVZR';
const address = 'ZxCdxeu7oYRXYEgZVuDeFy74GFtYDFbvrFSJpL6TJQ3Z1mxZaKQLZsJHyVuCH4pQSr3rQunoH7cE4bjJSWmJqWSn1yGLxsfJH';
it('checking the correctness of the result', () => {
const result: DecodeTransactionResult = decodeTransaction(objectInJson, secretViewKey, publicSpendKey);
if (result.ok) {
const { ok, amount, paymentId } = result;
expect({ ok, amount, paymentId }).toEqual({
ok: true,
amount: '0.0005',
paymentId: '49c925855b863a25'
});
}
});
it('checking the correctness of the result using address param', () => {
const result: DecodeTransactionResult = decodeTransaction(objectInJson, secretViewKey, address);
if (result.ok) {
const { ok, amount, paymentId } = result;
expect({ ok, amount, paymentId }).toEqual({
ok: true,
amount: '0.0005',
paymentId: '49c925855b863a25'
});
}
});
it('returned paymentId should be correct', () => {
const result: DecodeTransactionResult = decodeTransaction(objectInJson, secretViewKey, publicSpendKey);
if (result.ok) {
const { paymentId } = result;
const receivedIntegratedAddress: string = createIntegratedAddress(address, paymentId);
expect(integratedAddress).toEqual(receivedIntegratedAddress);
}
});
},
);

1
tests/fixtures.ts Normal file

File diff suppressed because one or more lines are too long

7
tsconfig.build.json Normal file
View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*"
],
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

18
tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"target": "es2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true
}
}

5078
yarn.lock Normal file

File diff suppressed because it is too large Load diff