feature: basic zano utils js methods
This commit is contained in:
parent
49e15a223a
commit
f73cc45c39
44 changed files with 10482 additions and 0 deletions
110
.eslintrc.json
Normal file
110
.eslintrc.json
Normal 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
47
.gitignore
vendored
Normal 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
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
18
|
||||
425
README.md
425
README.md
|
|
@ -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
76
package.json
Normal 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"
|
||||
}
|
||||
170
src/account/account-utils.ts
Normal file
170
src/account/account-utils.ts
Normal 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
1
src/account/constants.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const BRAINWALLET_DEFAULT_SEED_SIZE = 32;
|
||||
22
src/account/types.ts
Normal file
22
src/account/types.ts
Normal 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;
|
||||
}
|
||||
257
src/address/address-utils.ts
Normal file
257
src/address/address-utils.ts
Normal 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
23
src/address/constants.ts
Normal 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
16
src/address/types.ts
Normal 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
254
src/core/arx.ts
Normal 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
146
src/core/base58.ts
Normal 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;
|
||||
}
|
||||
70
src/core/binary-archive.ts
Normal file
70
src/core/binary-archive.ts
Normal 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
83
src/core/chacha8.ts
Normal 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
30
src/core/crypto-data.ts
Normal 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
504
src/core/crypto.ts
Normal 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
86
src/core/helpers.ts
Normal 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
78
src/core/interfaces.ts
Normal 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
25
src/core/serialize.ts
Normal 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
5
src/core/types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export type SpendKeypair = {
|
||||
seedKey?: string;
|
||||
secretSpendKey: string;
|
||||
publicSpendKey: string;
|
||||
}
|
||||
25
src/decode/constants.ts
Normal file
25
src/decode/constants.ts
Normal 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/;
|
||||
245
src/decode/decode-service.ts
Normal file
245
src/decode/decode-service.ts
Normal 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
11
src/decode/types.ts
Normal 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 }
|
||||
45
src/decode/utils/functions.ts
Normal file
45
src/decode/utils/functions.ts
Normal 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
34
src/index.ts
Normal 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';
|
||||
1628
src/mnemonic/consts/phrases.ts
Normal file
1628
src/mnemonic/consts/phrases.ts
Normal file
File diff suppressed because it is too large
Load diff
4
src/mnemonic/index.ts
Normal file
4
src/mnemonic/index.ts
Normal 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';
|
||||
87
src/mnemonic/mnemonic-to-seed.ts
Normal file
87
src/mnemonic/mnemonic-to-seed.ts
Normal 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;
|
||||
}
|
||||
100
src/mnemonic/seed-to-mnemonic.ts
Normal file
100
src/mnemonic/seed-to-mnemonic.ts
Normal 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
2
src/mnemonic/types.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export type MnemonicToSeedResult = string | false;
|
||||
export type SeedToMnemonicResult = string;
|
||||
8
src/transaction/constants.ts
Normal file
8
src/transaction/constants.ts
Normal 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',
|
||||
);
|
||||
152
src/transaction/transaction-utils.ts
Normal file
152
src/transaction/transaction-utils.ts
Normal 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,
|
||||
};
|
||||
2
src/transaction/types/transactions/index.ts
Normal file
2
src/transaction/types/transactions/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './v2';
|
||||
export * from './v3';
|
||||
60
src/transaction/types/transactions/v2.ts
Normal file
60
src/transaction/types/transactions/v2.ts
Normal 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;
|
||||
};
|
||||
101
src/transaction/types/transactions/v3.ts
Normal file
101
src/transaction/types/transactions/v3.ts
Normal 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
130
tests/account-utils.spec.ts
Normal 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
145
tests/address-utils.spec.ts
Normal 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
118
tests/address.ts
Normal 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',
|
||||
);
|
||||
})();
|
||||
52
tests/decode-utils.spec.ts
Normal file
52
tests/decode-utils.spec.ts
Normal 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
1
tests/fixtures.ts
Normal file
File diff suppressed because one or more lines are too long
7
tsconfig.build.json
Normal file
7
tsconfig.build.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue