@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
.eslintrc.js
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": [ "@generalprotocols/eslint-config/typescript" ],
|
||||||
|
"parserOptions":
|
||||||
|
{
|
||||||
|
"project": "./tsconfig.json",
|
||||||
|
"ecmaVersion": "2018",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"env":
|
||||||
|
{
|
||||||
|
"jest": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
module.exports = {
|
||||||
|
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
|
||||||
|
// This option interrupts the configuration hierarchy at this file
|
||||||
|
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
|
||||||
|
root: true,
|
||||||
|
|
||||||
|
// https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
|
||||||
|
// Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
|
||||||
|
// `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
|
||||||
|
parserOptions: {
|
||||||
|
parser: require.resolve('@typescript-eslint/parser'),
|
||||||
|
extraFileExtensions: [ '.vue' ]
|
||||||
|
},
|
||||||
|
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Rules order is important, please avoid shuffling them
|
||||||
|
extends: [
|
||||||
|
// Base ESLint recommended rules
|
||||||
|
// 'eslint:recommended',
|
||||||
|
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
|
||||||
|
// ESLint typescript rules
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
|
||||||
|
// https://github.com/prettier/eslint-config-prettier#installation
|
||||||
|
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
// required to apply rules which need type information
|
||||||
|
'@typescript-eslint',
|
||||||
|
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
|
||||||
|
// Prettier has not been included as plugin to avoid performance impact
|
||||||
|
// add it as an extension for your IDE
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
|
||||||
|
'prefer-promise-reject-errors': 'off',
|
||||||
|
|
||||||
|
quotes: ['warn', 'single', { avoidEscape: true }],
|
||||||
|
|
||||||
|
// this rule, if on, would require explicit return type on the `render` function
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
|
||||||
|
// in plain CommonJS modules, you can't use `import foo = require('foo')` to pass this rule, so it has to be disabled
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
|
||||||
|
// The core 'no-unused-vars' rules (in the eslint:recommended ruleset)
|
||||||
|
// does not work with type definitions
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
|
||||||
|
// allow debugger during development only
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules/
|
||||||
|
.nyc_output/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
public/
|
||||||
|
.vscode
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
||||||
|
unit-tests.xml
|
||||||
|
e2e-tests.xml
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# pnpm-related options
|
||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"tags": {
|
||||||
|
"allowUnknownTags": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"include": ["lib/", "./README.md"],
|
||||||
|
"includePattern": "\\.(jsx|js|ts|tsx)$",
|
||||||
|
"excludePattern": "(node_modules/|docs)"
|
||||||
|
},
|
||||||
|
"plugins": ["node_modules/better-docs/typescript"],
|
||||||
|
"opts": {
|
||||||
|
"template": "node_modules/docdash",
|
||||||
|
"encoding": "utf8",
|
||||||
|
"destination": "public/",
|
||||||
|
"recurse": true,
|
||||||
|
"verbose": false
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"cleverLinks": false,
|
||||||
|
"monospaceLinks": false,
|
||||||
|
"default": {
|
||||||
|
"outputSourceFiles": true,
|
||||||
|
"includeDate": true,
|
||||||
|
"useLongnameInNav": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"docdash": {
|
||||||
|
"collapse": false,
|
||||||
|
"static": true,
|
||||||
|
"sort": true,
|
||||||
|
"search": false,
|
||||||
|
"private": false,
|
||||||
|
"typedefs": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "@xo/stack",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "XO Stack",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.cjs.js",
|
||||||
|
"module": "dist/index.mjs.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run clean && npm run compile && npm run rollup && npm run post-processing",
|
||||||
|
"compile": "tsc",
|
||||||
|
"clean": "del dist",
|
||||||
|
"post-processing": "del ./dist/module",
|
||||||
|
"docs": "jsdoc -c jsdoc.json",
|
||||||
|
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"rollup": "rollup -c",
|
||||||
|
"spellcheck": "cspell '**'",
|
||||||
|
"test": "ava"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://gitlab.com/GeneralProtocols/xo/stack"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"bitcoin cash"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"author": "GeneralProtocols",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://gitlab.com/GeneralProtocols/xo/stack"
|
||||||
|
},
|
||||||
|
"homepage": "https://gitlab.com/GeneralProtocols/xo/stack",
|
||||||
|
"dependencies": {
|
||||||
|
"@bitauth/libauth": "^3.0.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||||
|
"@typescript-eslint/parser": "^6.3.0",
|
||||||
|
"eslint": "^8.10.0",
|
||||||
|
"eslint-config-prettier": "^8.1.0",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"rollup": "^3.29.2",
|
||||||
|
"rollup-plugin-dts": "^6.0.2",
|
||||||
|
"tsc-watch": "^6.0.4",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"ava": {
|
||||||
|
"extensions": {
|
||||||
|
"ts": "module"
|
||||||
|
},
|
||||||
|
"nodeArguments": [
|
||||||
|
"--loader=ts-node/esm"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Import package.json to reduce name repetition.
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import pkg from './package.json';
|
||||||
|
import dts from 'rollup-plugin-dts';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// Build main module in both ES and CJS
|
||||||
|
{
|
||||||
|
// Specify entry point for building
|
||||||
|
input: 'dist/module/lib/index.js',
|
||||||
|
|
||||||
|
// Omit all dependencies and peer dependencies from the final bundle
|
||||||
|
external: [
|
||||||
|
...Object.keys(pkg.dependencies || {}),
|
||||||
|
...Object.keys(pkg.peerDependencies || {}),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Produce outputs for ES and CJS
|
||||||
|
output: [
|
||||||
|
{ file: pkg.module, format: 'es' },
|
||||||
|
{ file: pkg.main, format: 'cjs' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Build type definitions for the library
|
||||||
|
{
|
||||||
|
input: 'dist/module/lib/index.d.ts',
|
||||||
|
output: [{ file: pkg.typings, format: 'es' }],
|
||||||
|
plugins: [dts()],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
import {
|
||||||
|
Base58AddressFormatVersion,
|
||||||
|
addressContentsToLockingBytecode,
|
||||||
|
lockingBytecodeToAddressContents,
|
||||||
|
isHex,
|
||||||
|
binToHex,
|
||||||
|
hexToBin,
|
||||||
|
decodeBase58Address,
|
||||||
|
decodeCashAddress,
|
||||||
|
disassembleBytecodeBCH,
|
||||||
|
encodeBase58AddressFormat,
|
||||||
|
encodeCashAddress,
|
||||||
|
sha256,
|
||||||
|
ripemd160,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
import type { KnownAddressTypeContents } from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export type AddressType = 'P2PK' | 'P2PKH' | 'P2SH20' | 'P2SH32';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Entity.
|
||||||
|
*/
|
||||||
|
export class Address {
|
||||||
|
public readonly addressContents: KnownAddressTypeContents;
|
||||||
|
|
||||||
|
constructor(addressContents: KnownAddressTypeContents) {
|
||||||
|
this.addressContents = addressContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a CashAddr into an Address Entity.
|
||||||
|
*
|
||||||
|
* @param address {string} The CashAddr to decode.
|
||||||
|
*
|
||||||
|
* @throws {Error} If address cannot be decoded.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const address = Address.fromCashAddr('bitcoincash:qpeqv9k9j3l7fepp9du24f9pr5jdu7c2mvn4wwldav')
|
||||||
|
*/
|
||||||
|
public static fromCashAddr(cashAddr: string): Address {
|
||||||
|
// CashAddr
|
||||||
|
const decodeResult = decodeCashAddress(cashAddr);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof decodeResult === 'string') {
|
||||||
|
throw new Error(decodeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the encode type in advance.
|
||||||
|
let encodeType: AddressType;
|
||||||
|
|
||||||
|
// Determine the encoding to use based on the Decoding Result's Type.
|
||||||
|
switch (decodeResult.type) {
|
||||||
|
case 'p2pkh':
|
||||||
|
encodeType = 'P2PKH';
|
||||||
|
break;
|
||||||
|
case 'p2sh':
|
||||||
|
encodeType = decodeResult.payload.length === 32 ? 'P2SH32' : 'P2SH20';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported address type: ${decodeResult.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return new instance of address.
|
||||||
|
return new Address({ payload: decodeResult.payload, type: encodeType });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a Legacy Address into an Address Entity.
|
||||||
|
*
|
||||||
|
* @param address {string} The Legacy (Base58) Address to decode.
|
||||||
|
*
|
||||||
|
* @throws {Error} If address cannot be decoded.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*/
|
||||||
|
public static fromLegacy(legacyAddress: string): Address {
|
||||||
|
// Decode the Legacy (Base58) Address.
|
||||||
|
const decodeResult = decodeBase58Address(legacyAddress);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof decodeResult === 'string') {
|
||||||
|
throw new Error(decodeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the encode type in advance.
|
||||||
|
let encodeType: AddressType;
|
||||||
|
|
||||||
|
// Determine the encoding to use based on the Decoding Result's Type.
|
||||||
|
switch (decodeResult.version) {
|
||||||
|
case Base58AddressFormatVersion.p2pkh:
|
||||||
|
case Base58AddressFormatVersion.p2pkhTestnet:
|
||||||
|
encodeType = 'P2PKH';
|
||||||
|
break;
|
||||||
|
case Base58AddressFormatVersion.p2sh20:
|
||||||
|
case Base58AddressFormatVersion.p2sh20Testnet:
|
||||||
|
encodeType = 'P2SH20';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported address type: ${decodeResult.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Address({ payload: decodeResult.payload, type: encodeType });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a Cash Address or Legacy Address.
|
||||||
|
*
|
||||||
|
* @param address {string} The address to attempt to decode.
|
||||||
|
*
|
||||||
|
* @throws {Error} If address cannot be decoded.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*/
|
||||||
|
public static fromCashAddrOrLegacy(address: string): Address {
|
||||||
|
// Attempt to decode as a Cash Address.
|
||||||
|
try {
|
||||||
|
return Address.fromCashAddr(address);
|
||||||
|
} catch (error) {
|
||||||
|
// And if that fails, attempt to decode as a Base58 Address.
|
||||||
|
return Address.fromLegacy(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate an address from Contract Bytecode.
|
||||||
|
*
|
||||||
|
* @param contractBytes {Uint8Array} The contract bytecode
|
||||||
|
*
|
||||||
|
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*/
|
||||||
|
public static fromRedeemScript(
|
||||||
|
contractBytes: Uint8Array,
|
||||||
|
type: 'P2SH20' | 'P2SH32' = 'P2SH32'
|
||||||
|
): Address {
|
||||||
|
// Do an initial SHA256 on the Contract Bytes.
|
||||||
|
let payload = sha256.hash(contractBytes);
|
||||||
|
|
||||||
|
if (type === 'P2SH20') {
|
||||||
|
// RIPEMD160 the SHA256 hash.
|
||||||
|
payload = ripemd160.hash(payload);
|
||||||
|
} else if (type === 'P2SH32') {
|
||||||
|
// SHA256 Hash the SHA256 hash a seconr time.
|
||||||
|
payload = sha256.hash(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new instance of address.
|
||||||
|
return new Address({ payload, type });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate an address from Contract Bytecode as a hex string.
|
||||||
|
*
|
||||||
|
* @param contractBytes {Uint8Array} The contract bytecode
|
||||||
|
*
|
||||||
|
* @throws {Error} If address cannot be instantiated from Contract Bytecode.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*/
|
||||||
|
public static fromRedeemScriptHex(
|
||||||
|
hex: string,
|
||||||
|
type: 'P2SH20' | 'P2SH32' = 'P2SH32'
|
||||||
|
): Address {
|
||||||
|
if (!isHex(hex)) {
|
||||||
|
throw new Error('Invalid hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractBytecode = hexToBin(hex);
|
||||||
|
|
||||||
|
// Return a new instance of address.
|
||||||
|
return Address.fromRedeemScript(contractBytecode, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to instantiate Address from Locking Bytecode.
|
||||||
|
*
|
||||||
|
* @param lockScript {Uint8Array} The lockscript bytes.
|
||||||
|
*
|
||||||
|
* @throws {Error} If lock script cannot be decoded into an address.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*/
|
||||||
|
public static fromLockscriptBytes(lockScript: Uint8Array): Address {
|
||||||
|
const addressContents = lockingBytecodeToAddressContents(lockScript);
|
||||||
|
|
||||||
|
// If the type is not recognized, throw an error...
|
||||||
|
if (addressContents.type === 'unknown') {
|
||||||
|
throw new Error(`Invalid type (${addressContents.type})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Address(addressContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to instantiate Address from Locking Bytecode as a hex string.
|
||||||
|
*
|
||||||
|
* @param hex {Uint8Array} The lockscript bytes as hex.
|
||||||
|
*
|
||||||
|
* @throws {Error} If lock script cannot be decoded into an address.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*/
|
||||||
|
public static fromLockscriptHex(hex: string): Address {
|
||||||
|
if (!isHex(hex)) {
|
||||||
|
throw new Error('Invalid hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockScriptBytes = hexToBin(hex);
|
||||||
|
|
||||||
|
return Address.fromLockscriptBytes(lockScriptBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate an address from a Ripemd160 hash.
|
||||||
|
*
|
||||||
|
* @param hash160 {Uint8Array} The hash to use.
|
||||||
|
* @param type {AddressType} The type of the address to instantiate (e.g. P2PKH).
|
||||||
|
*
|
||||||
|
* @throws {Error} If hash160 is not 20 bytes.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*/
|
||||||
|
public static fromHash160(
|
||||||
|
hash160: Uint8Array,
|
||||||
|
type: AddressType = 'P2PKH'
|
||||||
|
): Address {
|
||||||
|
// If the Hash160 bytes is not 20 in length, it is invalid...
|
||||||
|
if (hash160.length !== 20) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid hash160 length: Expected 20 (Received ${hash160.length})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Address({ payload: hash160, type });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate an address from a Ripemd160 hash (as hex).
|
||||||
|
*
|
||||||
|
* @param hash160Hex {string} The hash to use (as hex).
|
||||||
|
* @param type {AddressType} The type of the address to instantiate (e.g. P2PKH).
|
||||||
|
*
|
||||||
|
* @throws {Error} If hash160 is not 20 bytes.
|
||||||
|
*
|
||||||
|
* @returns {Address} Instance of Address.
|
||||||
|
*/
|
||||||
|
public static fromHash160Hex(
|
||||||
|
hash160Hex: string,
|
||||||
|
type: AddressType = 'P2PKH'
|
||||||
|
): Address {
|
||||||
|
if (!isHex(hash160Hex)) {
|
||||||
|
throw new Error('Invalid hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash160Bytes = hexToBin(hash160Hex);
|
||||||
|
|
||||||
|
return Address.fromHash160(hash160Bytes, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isValid(address: string): boolean {
|
||||||
|
try {
|
||||||
|
Address.fromCashAddrOrLegacy(address);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isValidLegacy(address: string): boolean {
|
||||||
|
try {
|
||||||
|
Address.fromLegacy(address);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isValidCashAddr(address: string): boolean {
|
||||||
|
try {
|
||||||
|
Address.fromCashAddr(address);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the type of address (P2PKH, P2SH).
|
||||||
|
*
|
||||||
|
* @returns {AddressType} The type of Address.
|
||||||
|
*/
|
||||||
|
public type(): AddressType {
|
||||||
|
return this.addressContents.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets this address' raw Hash160 (Ripemd160) hash as a Uint8Array.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The Hash160 of this address.
|
||||||
|
*/
|
||||||
|
public toHash160Bytes(): Uint8Array {
|
||||||
|
return this.addressContents.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets this address' raw Hash160 (Ripemd16) hash as a hex string.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The Hash160 of this address.
|
||||||
|
*/
|
||||||
|
public toHash160Hex(): string {
|
||||||
|
return binToHex(this.addressContents.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this address to CashAddr format.
|
||||||
|
*
|
||||||
|
* @param networkPrefix {'bitcoincash' | 'bchtest' | 'bchreg'} The Cash Address Network prefix.
|
||||||
|
*
|
||||||
|
* @throws {Error} If address cannot be encoded as a CashAddr.
|
||||||
|
*
|
||||||
|
* @returns {string} The address in CashAddr format.
|
||||||
|
*/
|
||||||
|
public toCashAddr(
|
||||||
|
networkPrefix: 'bitcoincash' | 'bchtest' | 'bchreg' = 'bitcoincash',
|
||||||
|
supportTokens = false
|
||||||
|
): string {
|
||||||
|
// Define the encode type in advance.
|
||||||
|
let encodeType: 'p2pkh' | 'p2sh' | 'p2pkhWithTokens' | 'p2shWithTokens';
|
||||||
|
|
||||||
|
// Determine the encoding to use based on the Address Content's Type.
|
||||||
|
switch (this.addressContents.type) {
|
||||||
|
case 'P2PKH':
|
||||||
|
encodeType = supportTokens ? 'p2pkhWithTokens' : 'p2pkh';
|
||||||
|
break;
|
||||||
|
case 'P2SH20':
|
||||||
|
encodeType = supportTokens ? 'p2shWithTokens' : 'p2sh';
|
||||||
|
break;
|
||||||
|
case 'P2SH32':
|
||||||
|
encodeType = supportTokens ? 'p2shWithTokens' : 'p2sh';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported address type: ${this.addressContents.type}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This function will throw if the payload is invalid.
|
||||||
|
return encodeCashAddress({
|
||||||
|
prefix: networkPrefix,
|
||||||
|
type: encodeType,
|
||||||
|
payload: this.addressContents.payload,
|
||||||
|
}).address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this address to Legacy (Base58) Address format.
|
||||||
|
*
|
||||||
|
* @throws {Error} If address cannot be encoded as a Legacy Address (Base58).
|
||||||
|
*
|
||||||
|
* @returns {string} The address in Legacy Address format.
|
||||||
|
*/
|
||||||
|
public toLegacy(): string {
|
||||||
|
// Define the encode type in advance.
|
||||||
|
let encodeType: Base58AddressFormatVersion;
|
||||||
|
|
||||||
|
// Determine the encoding to use based on the Address Content's Type.
|
||||||
|
switch (this.addressContents.type) {
|
||||||
|
case 'P2PKH':
|
||||||
|
encodeType = Base58AddressFormatVersion.p2pkh;
|
||||||
|
break;
|
||||||
|
case 'P2SH20':
|
||||||
|
encodeType = Base58AddressFormatVersion.p2sh20;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported address type: ${this.addressContents.type}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the address in Base58 format.
|
||||||
|
return encodeBase58AddressFormat(encodeType, this.addressContents.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the corresponding lockscript for this address as a Uint8Array.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The lockscript as a Uint8Array.
|
||||||
|
*/
|
||||||
|
public toLockscriptBytes(): Uint8Array {
|
||||||
|
return addressContentsToLockingBytecode(this.addressContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the corresponding lockscript for this address as a hex string.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The lockscript as a hex string.
|
||||||
|
*/
|
||||||
|
public toLockscriptHex(): string {
|
||||||
|
return binToHex(this.toLockscriptBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the corresponding lockscript for this address as ASM.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The lockscript as ASN.
|
||||||
|
*/
|
||||||
|
public toLockscriptAsm(): string {
|
||||||
|
const lockscriptBytes = this.toLockscriptBytes();
|
||||||
|
|
||||||
|
const lockscriptAsm = disassembleBytecodeBCH(lockscriptBytes);
|
||||||
|
|
||||||
|
return lockscriptAsm;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { HDPublicNode } from './hd-public-node.js';
|
||||||
|
import { PrivateKey } from './private-key.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
encodeHdPrivateKey,
|
||||||
|
decodeHdPrivateKey,
|
||||||
|
deriveHdPath,
|
||||||
|
deriveHdPublicNode,
|
||||||
|
deriveHdPrivateNodeFromSeed,
|
||||||
|
generateRandomBytes,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DecodedHdKey,
|
||||||
|
HdKeyNetwork,
|
||||||
|
HdPrivateNodeValid,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hierarchically Deterministic Private Node Entity.
|
||||||
|
*/
|
||||||
|
export class HDPrivateNode {
|
||||||
|
// The HD Private Node.
|
||||||
|
public readonly node: HdPrivateNodeValid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new HD Private Node.
|
||||||
|
*
|
||||||
|
* @param node {HdPrivateNodeValid} The HD Private Node.
|
||||||
|
*/
|
||||||
|
constructor(node: HdPrivateNodeValid) {
|
||||||
|
this.node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HD Private Node from the given XPriv Key.
|
||||||
|
*
|
||||||
|
* @param xpriv {string} The XPriv Key.
|
||||||
|
*
|
||||||
|
* @throws {Error} If HD Private Node cannot be created.
|
||||||
|
*
|
||||||
|
* @returns {HDPrivateNode} The created HD Private Node.
|
||||||
|
*/
|
||||||
|
public static fromXPriv(xpriv: string): HDPrivateNode {
|
||||||
|
// Attempt to decode the XPriv Key.
|
||||||
|
const decodeResult = decodeHdPrivateKey(xpriv);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof decodeResult === 'string') {
|
||||||
|
throw new Error(decodeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new HD Private Node from the given XPriv.
|
||||||
|
return new HDPrivateNode(decodeResult.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HD Private Node from a raw seed.
|
||||||
|
*
|
||||||
|
* @param seed {Uint8Array} The raw seed bytes.
|
||||||
|
*
|
||||||
|
* @throws {Error} If HD Private Node cannot be created.
|
||||||
|
*
|
||||||
|
* @returns {HDPrivateNode} The created HD Private Node.
|
||||||
|
*/
|
||||||
|
public static fromSeed(seed: Uint8Array): HDPrivateNode {
|
||||||
|
// If the seed is not 32 bytes, throw an error.
|
||||||
|
/*if (seed.length !== 32) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to derive Private Node from seed: Seed must be 32 bytes (${seed.length} given)`
|
||||||
|
);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Attempt to derive HD Private Node from the given seed.
|
||||||
|
const deriveResult = deriveHdPrivateNodeFromSeed(seed);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof deriveResult === 'string') {
|
||||||
|
throw new Error(deriveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new HD Private Node from the given seed.
|
||||||
|
return new HDPrivateNode(deriveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random HD Private Node.
|
||||||
|
*
|
||||||
|
* @throws {Error} If HD Private Node cannot be created.
|
||||||
|
*
|
||||||
|
* @returns {HDPrivateNode} The created HD Private Node.
|
||||||
|
*/
|
||||||
|
public static generateRandom(): HDPrivateNode {
|
||||||
|
// Attempt to derive HD Private Node from randomly generated bytes.
|
||||||
|
const deriveResult = deriveHdPrivateNodeFromSeed(generateRandomBytes(32));
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof deriveResult === 'string') {
|
||||||
|
throw new Error(deriveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the new HD Private Node from the random bytes.
|
||||||
|
return new HDPrivateNode(deriveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the XPriv string for this node.
|
||||||
|
*
|
||||||
|
* @returns {string} The XPriv string for this node.
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
// Return the Mainnet XPriv.
|
||||||
|
return this.toXPriv();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Private Key entity for this node.
|
||||||
|
*
|
||||||
|
* @returns {PrivateKey} The Private Key Entity for this node.
|
||||||
|
*/
|
||||||
|
public privateKey(): PrivateKey {
|
||||||
|
// Return a Private Key Entity from the private key of our node.
|
||||||
|
return new PrivateKey(this.node.privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a HD Private Node from the given BIP32 path.
|
||||||
|
*
|
||||||
|
* @param path {string} The BIP32 Path (e.g. m/44'/145'/0'/0/1).
|
||||||
|
*
|
||||||
|
* @throws {Error} If HD Private Node cannot be derived.
|
||||||
|
*
|
||||||
|
* @returns {HDPrivateNode} The derived HD Private Node
|
||||||
|
*/
|
||||||
|
public derivePath(path: string): HDPrivateNode {
|
||||||
|
// Attempt to derive the given path.
|
||||||
|
const derivePathResult = deriveHdPath(this.node, path);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof derivePathResult === 'string') {
|
||||||
|
throw new Error(derivePathResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new HD Private Node Entity derived from the given path.
|
||||||
|
return new HDPrivateNode(derivePathResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a HD Public Node from this Private Node.
|
||||||
|
*
|
||||||
|
* @returns {HDPublicNode} The derived HD Public Node
|
||||||
|
*/
|
||||||
|
public deriveHDPublicNode(): HDPublicNode {
|
||||||
|
// Derive a HD Public Node from this HD Private Node.
|
||||||
|
const publicNode = deriveHdPublicNode(this.node);
|
||||||
|
|
||||||
|
// Return a new HD Public Node entity from our derived Public Node.
|
||||||
|
return new HDPublicNode(publicNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this node to an XPriv Key for export.
|
||||||
|
*
|
||||||
|
* @param network {HdKeyNetwork} The network to encode for.
|
||||||
|
*
|
||||||
|
* @returns {string} The XPriv Key.
|
||||||
|
*/
|
||||||
|
public toXPriv(network: HdKeyNetwork = 'mainnet'): string {
|
||||||
|
// Create our node info structure.
|
||||||
|
const nodeInfo: DecodedHdKey<HdPrivateNodeValid> = {
|
||||||
|
network: network,
|
||||||
|
node: this.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encode the XPriv from our node info.
|
||||||
|
return encodeHdPrivateKey(nodeInfo).hdPrivateKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { PublicKey } from './public-key.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
encodeHdPublicKey,
|
||||||
|
decodeHdPublicKey,
|
||||||
|
deriveHdPath,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DecodedHdKey,
|
||||||
|
HdKeyNetwork,
|
||||||
|
HdPublicNodeValid,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hierarchically Deterministic Public Node Entity.
|
||||||
|
*/
|
||||||
|
export class HDPublicNode {
|
||||||
|
// The HD Public Node.
|
||||||
|
public readonly node: HdPublicNodeValid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new HD Public Node.
|
||||||
|
*
|
||||||
|
* @param node {HdPublicNode} The HD Public Node.
|
||||||
|
*/
|
||||||
|
constructor(node: HdPublicNodeValid) {
|
||||||
|
this.node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HD Public Node from the given XPriv Key.
|
||||||
|
*
|
||||||
|
* @param xpub {string} The XPriv Key.
|
||||||
|
*
|
||||||
|
* @throws {Error} If HD Public Node cannot be created.
|
||||||
|
*
|
||||||
|
* @returns {HDPrivateNode} The created HD Public Node.
|
||||||
|
*/
|
||||||
|
public static fromXPub(xpub: string): HDPublicNode {
|
||||||
|
// Attempt to decode the XPub Key.
|
||||||
|
const decodeResult = decodeHdPublicKey(xpub);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof decodeResult === 'string') {
|
||||||
|
throw new Error(decodeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new HD Public Node from the given XPub.
|
||||||
|
return new HDPublicNode(decodeResult.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the XPub string for this node.
|
||||||
|
*
|
||||||
|
* @returns {string} The XPub string for this node.
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
// Return the Mainnet XPub.
|
||||||
|
return this.toXPub();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Public Key Entity for this node.
|
||||||
|
*
|
||||||
|
* @returns {PublicKey} The Public Key Entity for this node.
|
||||||
|
*/
|
||||||
|
public publicKey(): PublicKey {
|
||||||
|
// Return a Public Key Entity from the public key of our node.
|
||||||
|
return new PublicKey(this.node.publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a HD Public Node from the given BIP32 path.
|
||||||
|
*
|
||||||
|
* @param path {string} The BIP32 Path (e.g. m/44'/145'/0'/0/1).
|
||||||
|
*
|
||||||
|
* @throws {Error} If HD Public Node cannot be derived.
|
||||||
|
*
|
||||||
|
* @returns {HDPrivateNode} The derived HD Public Node
|
||||||
|
*/
|
||||||
|
public derivePath(path: string): HDPublicNode {
|
||||||
|
// Attempt to derive the given path.
|
||||||
|
const derivePathResult = deriveHdPath(this.node, path);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof derivePathResult === 'string') {
|
||||||
|
throw new Error(derivePathResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new HD Public Node Entity derived from the given path.
|
||||||
|
return new HDPublicNode(derivePathResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this node to an XPub Key for export.
|
||||||
|
*
|
||||||
|
* @param network {HdKeyNetwork} The network to encode for.
|
||||||
|
*
|
||||||
|
* @returns {string} The XPub Key.
|
||||||
|
*/
|
||||||
|
public toXPub(network: HdKeyNetwork = 'mainnet'): string {
|
||||||
|
// Create our node info structure.
|
||||||
|
const nodeInfo = {
|
||||||
|
network: network,
|
||||||
|
node: this.node,
|
||||||
|
} as DecodedHdKey<HdPublicNodeValid>;
|
||||||
|
|
||||||
|
// Encode the XPub from our node info.
|
||||||
|
return encodeHdPublicKey(nodeInfo).hdPublicKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './address.js';
|
||||||
|
export * from './hd-private-node.js';
|
||||||
|
export * from './hd-public-node.js';
|
||||||
|
export * from './private-key.js';
|
||||||
|
export * from './public-key.js';
|
||||||
|
export * from './satoshis.js';
|
||||||
|
export * from './transaction.js';
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import { PublicKey } from './public-key.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
binToHex,
|
||||||
|
decodePrivateKeyWif,
|
||||||
|
encodePrivateKeyWif,
|
||||||
|
generatePrivateKey,
|
||||||
|
hexToBin,
|
||||||
|
isHex,
|
||||||
|
binToUtf8,
|
||||||
|
utf8ToBin,
|
||||||
|
bigIntToCompactUint,
|
||||||
|
secp256k1,
|
||||||
|
sha256,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
RecoverableSignature,
|
||||||
|
WalletImportFormatType,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private Key entity.
|
||||||
|
*/
|
||||||
|
export class PrivateKey {
|
||||||
|
// The raw bytes representing the Private Key.
|
||||||
|
public readonly bytes: Uint8Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new Private Key.
|
||||||
|
*
|
||||||
|
* @throws {Error} If Private Key cannot be constructed.
|
||||||
|
*
|
||||||
|
* @param bytes {Uint8Array} The raw bytes (32) for the Private Key.
|
||||||
|
*/
|
||||||
|
constructor(bytes: Uint8Array) {
|
||||||
|
// Ensure that the Private Key is valid.
|
||||||
|
if (!secp256k1.validatePrivateKey(bytes)) {
|
||||||
|
throw new Error(
|
||||||
|
`Private Key is invalid: Must be >= 0x01 and <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Private Key Entity from the given bytes (as hexadecimal string).
|
||||||
|
*
|
||||||
|
* @param privateKeyHex {string} Raw Private Key bytes encoded as hexadecimal string.
|
||||||
|
*
|
||||||
|
* @throws {Error} If Private Key could not be created.
|
||||||
|
*
|
||||||
|
* @returns {PrivateKey} The created Private Key Entity.
|
||||||
|
*/
|
||||||
|
public static fromHex(privateKeyHex: string): PrivateKey {
|
||||||
|
// Ensure that the given string is hex.
|
||||||
|
if (!isHex(privateKeyHex)) {
|
||||||
|
throw new Error(
|
||||||
|
`The given string (${privateKeyHex}) is not valid hexadecimal.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the hex to binary (Uint8Array).
|
||||||
|
const privateKeyBin = hexToBin(privateKeyHex);
|
||||||
|
|
||||||
|
// Create a Private Key Entity from the binary.
|
||||||
|
return new PrivateKey(privateKeyBin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Private Key from a given WIF (Note that the network information from the WIF will be lost).
|
||||||
|
*
|
||||||
|
* @throws {Error} If Private Key cannot be created.
|
||||||
|
*
|
||||||
|
* @returns {PrivateKey} The Private Key
|
||||||
|
*/
|
||||||
|
public static fromWIF(
|
||||||
|
wif: string,
|
||||||
|
removePrefixes = ['bitcoincash:', 'wif:']
|
||||||
|
): PrivateKey {
|
||||||
|
// Remove the prefixes from the WIF (if they exist).
|
||||||
|
const wifWithoutPrefixes = removePrefixes.reduce((result, prefix) => {
|
||||||
|
return result.startsWith(prefix) ? result.slice(prefix.length) : result;
|
||||||
|
}, wif);
|
||||||
|
|
||||||
|
// Attempt to decode the WIF.
|
||||||
|
const decodeResult = decodePrivateKeyWif(wifWithoutPrefixes);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof decodeResult === 'string') {
|
||||||
|
throw new Error(decodeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new Private Key entity.
|
||||||
|
return new PrivateKey(decodeResult.privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random Private Key.
|
||||||
|
*
|
||||||
|
* @returns {PrivateKey} The randomly generated Private Key
|
||||||
|
*/
|
||||||
|
public static generateRandom(): PrivateKey {
|
||||||
|
const bytes = generatePrivateKey();
|
||||||
|
|
||||||
|
return new PrivateKey(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the public key from this private key.
|
||||||
|
*
|
||||||
|
* @throws {Error} If Public Key cannot be derived.
|
||||||
|
*
|
||||||
|
* @returns {PublicKey} The derived Public Key.
|
||||||
|
*/
|
||||||
|
public derivePublicKey(): PublicKey {
|
||||||
|
// Attempt to derive the public key.
|
||||||
|
const deriveResult = secp256k1.derivePublicKeyCompressed(this.bytes);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof deriveResult === 'string') {
|
||||||
|
throw new Error(deriveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the derived Public Key.
|
||||||
|
return new PublicKey(deriveResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message hash with this Private Key using a Recoverable Compact Signature.
|
||||||
|
*
|
||||||
|
* @param messageHash {Uint8Array} The hash of the message.
|
||||||
|
*
|
||||||
|
* @throws {Error} If message hash cannot be signed.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The generated signature.
|
||||||
|
*/
|
||||||
|
public signMessageHashRecoverableCompact(
|
||||||
|
messageHash: Uint8Array
|
||||||
|
): RecoverableSignature {
|
||||||
|
// Attempt to sign the message.
|
||||||
|
const signResult = secp256k1.signMessageHashRecoverableCompact(
|
||||||
|
this.bytes,
|
||||||
|
messageHash
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof signResult === 'string') {
|
||||||
|
throw new Error(signResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the signature.
|
||||||
|
return signResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message hash with this Private Key using a Compact Signature.
|
||||||
|
*
|
||||||
|
* @param messageHash {Uint8Array} The hash of the message.
|
||||||
|
*
|
||||||
|
* @throws {Error} If message hash cannot be signed.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The generated signature.
|
||||||
|
*/
|
||||||
|
public signMessageHashCompact(messageHash: Uint8Array): Uint8Array {
|
||||||
|
// Attempt to sign the message.
|
||||||
|
const signResult = secp256k1.signMessageHashCompact(
|
||||||
|
this.bytes,
|
||||||
|
messageHash
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof signResult === 'string') {
|
||||||
|
throw new Error(signResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the signature.
|
||||||
|
return signResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message hash with this Private Key using a Schnorr Signature.
|
||||||
|
*
|
||||||
|
* @param messageHash {Uint8Array} The hash of the message.
|
||||||
|
*
|
||||||
|
* @throws {Error} If message hash cannot be signed.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The generated signature.
|
||||||
|
*/
|
||||||
|
public signMessageHashSchnorr(messageHash: Uint8Array): Uint8Array {
|
||||||
|
// Attempt to sign the message.
|
||||||
|
const signResult = secp256k1.signMessageHashSchnorr(
|
||||||
|
this.bytes,
|
||||||
|
messageHash
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof signResult === 'string') {
|
||||||
|
throw new Error(signResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the signature.
|
||||||
|
return signResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message hash with this Private Key using a DER Signature.
|
||||||
|
*
|
||||||
|
* @param messageHash {Uint8Array} The hash of the message.
|
||||||
|
*
|
||||||
|
* @throws {Error} If message hash cannot be signed.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The generated signature.
|
||||||
|
*/
|
||||||
|
public signMessageHashDER(messageHash: Uint8Array): Uint8Array {
|
||||||
|
const signResult = secp256k1.signMessageHashDER(this.bytes, messageHash);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof signResult === 'string') {
|
||||||
|
throw new Error(signResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the signature.
|
||||||
|
return signResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message hash with this Private Key using an Electron Cash Signature.
|
||||||
|
*
|
||||||
|
* This applies the Bitcoin and Electron Magic Bytes.
|
||||||
|
*
|
||||||
|
* NOTE: Electron can verify this message successfully, but generates them differently internally.
|
||||||
|
*
|
||||||
|
* @param messageHash {Uint8Array} The hash of the message.
|
||||||
|
*
|
||||||
|
* @throws {Error} If message hash cannot be signed.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The generated signature.
|
||||||
|
*/
|
||||||
|
public signMessageElectron(message: string): Uint8Array {
|
||||||
|
// Compile the message.
|
||||||
|
const magicBytes = utf8ToBin('\x18Bitcoin Signed Message:\n');
|
||||||
|
const messageBytes = utf8ToBin(message);
|
||||||
|
const messageLengthBytes = bigIntToCompactUint(BigInt(messageBytes.length)); // hexToBin(messageBytes.length.toString(16)); //
|
||||||
|
const messageFinal = new Uint8Array([
|
||||||
|
...magicBytes,
|
||||||
|
...messageLengthBytes,
|
||||||
|
...messageBytes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Hash the message.
|
||||||
|
// NOTE: That we hash256 (2 x sha256) the message.
|
||||||
|
const messageHash = sha256.hash(sha256.hash(messageFinal));
|
||||||
|
|
||||||
|
// Use Recoverable Compact Signature.
|
||||||
|
const signature = this.signMessageHashRecoverableCompact(messageHash);
|
||||||
|
|
||||||
|
// Apply Electon encoding.
|
||||||
|
const electronEncoding = new Uint8Array([
|
||||||
|
...[31 + signature.recoveryId],
|
||||||
|
...signature.signature,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return the signature.
|
||||||
|
return electronEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the raw bytes of this Private Key to hexadecimal encoding.
|
||||||
|
*
|
||||||
|
* @returns {string} The raw bytes of the Private Key as a hexadecimal string.
|
||||||
|
*/
|
||||||
|
public toHex(): string {
|
||||||
|
// Return the hexadecimal encoded bytes of this Private Key.
|
||||||
|
return binToHex(this.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this Private Key to a raw 32 byte representation.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The raw Private Key as a Uint8Array.
|
||||||
|
*/
|
||||||
|
public toBytes(): Uint8Array {
|
||||||
|
// Return the raw bytes for this Private Key.
|
||||||
|
return this.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this Private Key to a WIF format.
|
||||||
|
*
|
||||||
|
* @param type {WalletImportFormatType} The type of WIF to create.
|
||||||
|
*
|
||||||
|
* @returns {string} The Private Key in WIF format.
|
||||||
|
*/
|
||||||
|
public toWif(type: WalletImportFormatType = 'mainnet'): string {
|
||||||
|
// Return the encoded Private Key WIF.
|
||||||
|
return encodePrivateKeyWif(this.bytes, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Private Key as a mainnet WIF.
|
||||||
|
*
|
||||||
|
* @returns {string} The Private Key as a mainnet WIF.
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
// Return the encoded Mainnet Private Key WIF.
|
||||||
|
return this.toWif();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { Address } from './address.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
binToHex,
|
||||||
|
hexToBin,
|
||||||
|
isHex,
|
||||||
|
secp256k1,
|
||||||
|
sha256,
|
||||||
|
ripemd160,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public Key Entity.
|
||||||
|
*/
|
||||||
|
export class PublicKey {
|
||||||
|
// The raw bytes representing the Public Key.
|
||||||
|
public readonly bytes: Uint8Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new Public Key.
|
||||||
|
*
|
||||||
|
* @param bytes {Uint8Array} The raw bytes (33) for the Public Key.
|
||||||
|
*/
|
||||||
|
constructor(bytes: Uint8Array) {
|
||||||
|
// Ensure that exactly 33 Bytes are given.
|
||||||
|
if (!secp256k1.validatePublicKey(bytes)) {
|
||||||
|
throw new Error(`Invalid Public Key.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Public Key entity from the given hexadecimal string.
|
||||||
|
*
|
||||||
|
* @param publicKeyHex {string} Hexadecimal encoding of the Public Key bytes.
|
||||||
|
*
|
||||||
|
* @throws {Error} If Public Key cannot be created.
|
||||||
|
*
|
||||||
|
* @returns {PublicKey} The created Public Key entity.
|
||||||
|
*/
|
||||||
|
public static fromHex(publicKeyHex: string): PublicKey {
|
||||||
|
// Ensure that the given string is hex.
|
||||||
|
if (!isHex(publicKeyHex)) {
|
||||||
|
throw new Error(
|
||||||
|
`The given string (${publicKeyHex}) is not valid hexadecimal.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the hex to binary (Uint8Array).
|
||||||
|
const publicKey = hexToBin(publicKeyHex);
|
||||||
|
|
||||||
|
// Create a Public Key Entity from the binary.
|
||||||
|
return new PublicKey(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the Hash160 for this Public Key.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The Hash160 of the Public Key.
|
||||||
|
*/
|
||||||
|
public deriveHash160(): Uint8Array {
|
||||||
|
// SHA256 Hash the public key.
|
||||||
|
const sha256Hash = sha256.hash(this.bytes);
|
||||||
|
|
||||||
|
// RIPEMD160 the SHA256 hash.
|
||||||
|
const ripemd160Hash = ripemd160.hash(sha256Hash);
|
||||||
|
|
||||||
|
// And then return it.
|
||||||
|
return ripemd160Hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the Hash160 as Hex for this Public Key.
|
||||||
|
*
|
||||||
|
* @returns {string} The Hash160 of the Public Key as hex.
|
||||||
|
*/
|
||||||
|
public deriveHash160Hex(): string {
|
||||||
|
const hash160Bytes = this.deriveHash160();
|
||||||
|
|
||||||
|
return binToHex(hash160Bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the Address for this Public Key.
|
||||||
|
*
|
||||||
|
* @returns {Address} The Address for this Public Key.
|
||||||
|
*/
|
||||||
|
public deriveAddress(): Address {
|
||||||
|
// Get the RIPEMD160 Hash of the Public Key.
|
||||||
|
const ripemd160Hash = this.deriveHash160();
|
||||||
|
|
||||||
|
// Return an instance of address
|
||||||
|
return Address.fromHash160(ripemd160Hash, 'P2PKH');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a compact message hash was signed with the corresponding Private Key.
|
||||||
|
*
|
||||||
|
* @param messageHash {Uint8Array} The hash of the message.
|
||||||
|
* @param signature {Uint8Array) The signature of the message.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if signature and message hash verifies, false otherwise..
|
||||||
|
*/
|
||||||
|
public verifyMessageHashCompact(
|
||||||
|
messageHash: Uint8Array,
|
||||||
|
signature: Uint8Array
|
||||||
|
): boolean {
|
||||||
|
const validity = secp256k1.verifySignatureCompact(
|
||||||
|
signature,
|
||||||
|
this.bytes,
|
||||||
|
messageHash
|
||||||
|
);
|
||||||
|
|
||||||
|
return validity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a DER message hash was signed with the corresponding Private Key.
|
||||||
|
*
|
||||||
|
* @param messageHash {Uint8Array} The hash of the message.
|
||||||
|
* @param signature {Uint8Array) The signature of the message.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if signature and message hash verifies, false otherwise..
|
||||||
|
*/
|
||||||
|
public verifyMessageHashDER(
|
||||||
|
messageHash: Uint8Array,
|
||||||
|
signature: Uint8Array
|
||||||
|
): boolean {
|
||||||
|
const validity = secp256k1.verifySignatureDER(
|
||||||
|
signature,
|
||||||
|
this.bytes,
|
||||||
|
messageHash
|
||||||
|
);
|
||||||
|
|
||||||
|
return validity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a Schnorr message hash was signed with the corresponding Private Key.
|
||||||
|
*
|
||||||
|
* @param messageHash {Uint8Array} The hash of the message.
|
||||||
|
* @param signature {Uint8Array) The signature of the message.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if signature and message hash verifies, false otherwise..
|
||||||
|
*/
|
||||||
|
public verifyMessageHashSchnorr(
|
||||||
|
messageHash: Uint8Array,
|
||||||
|
signature: Uint8Array
|
||||||
|
): boolean {
|
||||||
|
const validity = secp256k1.verifySignatureSchnorr(
|
||||||
|
signature,
|
||||||
|
this.bytes,
|
||||||
|
messageHash
|
||||||
|
);
|
||||||
|
|
||||||
|
return validity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the raw bytes of this Public Key to a hexadecimal representation.
|
||||||
|
*
|
||||||
|
* @returns {string} The raw bytes Public Key encoded as Hexadecimal.
|
||||||
|
*/
|
||||||
|
public toHex(): string {
|
||||||
|
return binToHex(this.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw bytes of this Public Key.
|
||||||
|
*
|
||||||
|
* @returns {Uint8Array} The raw bytes of the Public Key.
|
||||||
|
*/
|
||||||
|
public toBytes(): Uint8Array {
|
||||||
|
// Return the raw bytes for this Public Key.
|
||||||
|
return this.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the hexadecimal representation of the raw bytes of the Public Key.
|
||||||
|
*
|
||||||
|
* @returns {string} The raw bytes of the Public Key as hexadecimal.
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
// Return the raw Public Key byes encoded as a hexadecimal string.
|
||||||
|
return this.toHex();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Satoshis Primitive
|
||||||
|
*/
|
||||||
|
export class Satoshis {
|
||||||
|
public readonly satoshis: bigint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance of Satoshis.
|
||||||
|
*
|
||||||
|
* This is the same as using fromSatoshis().
|
||||||
|
*
|
||||||
|
* @param satoshis {number} The satoshi amount.
|
||||||
|
*/
|
||||||
|
constructor(satoshis: bigint | number) {
|
||||||
|
// Set the satoshi amount.
|
||||||
|
this.satoshis = BigInt(satoshis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates from a Satoshi Amount.
|
||||||
|
*
|
||||||
|
* @param satoshis {number} The Satoshi amount.
|
||||||
|
*
|
||||||
|
* @returns {Satoshis} New instance of Satoshis.
|
||||||
|
*/
|
||||||
|
public static fromSats(satoshis: bigint | number): Satoshis {
|
||||||
|
return new Satoshis(satoshis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates from a Satoshi Amount.
|
||||||
|
*
|
||||||
|
* @param satoshis {number} The Satoshi amount.
|
||||||
|
*
|
||||||
|
* @deprecated Use fromSats() instead.
|
||||||
|
*
|
||||||
|
* @returns {Satoshis} New instance of Satoshis.
|
||||||
|
*/
|
||||||
|
public static fromSatoshis(satoshis: bigint | number): Satoshis {
|
||||||
|
return new Satoshis(satoshis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates from a Bits Amount (100 satoshis = 1 bit).
|
||||||
|
*
|
||||||
|
* @param bits {number} The Bits amount.
|
||||||
|
*
|
||||||
|
* @returns {Satoshis} New instance of Satoshis.
|
||||||
|
*/
|
||||||
|
public static fromBits(bits: number): Satoshis {
|
||||||
|
const satoshis = Satoshis.roundToDigits(bits * 100, 0);
|
||||||
|
|
||||||
|
return new Satoshis(satoshis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates from a mBCH Amount (100,000 satoshis = 1 mBCH).
|
||||||
|
*
|
||||||
|
* @param mBCH {number} The mBCH amount.
|
||||||
|
*
|
||||||
|
* @returns {Satoshis} New instance of Satoshis.
|
||||||
|
*/
|
||||||
|
public static fromMBCH(mBCH: number): Satoshis {
|
||||||
|
const satoshis = Satoshis.roundToDigits(mBCH * 100_000, 0);
|
||||||
|
|
||||||
|
return new Satoshis(satoshis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates from a BCH Amount (100,000,000 satoshis = 1 BCH).
|
||||||
|
*
|
||||||
|
* @param bch {number} The BCH amount.
|
||||||
|
*
|
||||||
|
* @returns {Satoshis} New instance of Satoshis.
|
||||||
|
*/
|
||||||
|
public static fromBCH(bch: number): Satoshis {
|
||||||
|
const satoshis = Satoshis.roundToDigits(bch * 100_000_000, 0);
|
||||||
|
|
||||||
|
return new Satoshis(satoshis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds a number to the given number of digits.
|
||||||
|
*
|
||||||
|
* @param numberToRound {number} The number to round.
|
||||||
|
* @param digits {number} The number of digits to round to.
|
||||||
|
*
|
||||||
|
* @returns {Number} The rounded number.
|
||||||
|
*/
|
||||||
|
public static roundToDigits(numberToRound: number, digits: number): number {
|
||||||
|
// Set the options of the Number Format object.
|
||||||
|
const options = {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: digits,
|
||||||
|
maximumFractionDigits: digits,
|
||||||
|
useGrouping: false,
|
||||||
|
numberingSystem: 'latn',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create an instance of number format using above options.
|
||||||
|
// NOTE: We force the locale to en-GB so that the number is formatted correctly (e.g. with a decimal, not a comma).
|
||||||
|
const numberFormat = new Intl.NumberFormat('en-GB', options);
|
||||||
|
|
||||||
|
// Format the number.
|
||||||
|
const formattedAmount = numberFormat.format(numberToRound);
|
||||||
|
|
||||||
|
// Return the formatted number.
|
||||||
|
return Number(formattedAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the amount to Satoshis (as a BigInt).
|
||||||
|
*
|
||||||
|
* @returns {bigint} The satoshi amount.
|
||||||
|
*/
|
||||||
|
public toBigInt(): bigint {
|
||||||
|
return this.satoshis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the amount to Satoshis.
|
||||||
|
*
|
||||||
|
* @returns {number} The satoshi amount.
|
||||||
|
*/
|
||||||
|
public toSats(): number {
|
||||||
|
return Number(this.satoshis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the amount to Satoshis.
|
||||||
|
*
|
||||||
|
* @deprecated Use toSats() instead.
|
||||||
|
*
|
||||||
|
* @returns {number} The satoshi amount.
|
||||||
|
*/
|
||||||
|
public toSatoshis(): number {
|
||||||
|
return Number(this.satoshis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the amount to Bits (100 satoshis = 1 bit).
|
||||||
|
*
|
||||||
|
* @returns {number} The bit amount.
|
||||||
|
*/
|
||||||
|
public toBits(): number {
|
||||||
|
return Satoshis.roundToDigits(Number(this.satoshis) / 100, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the amount to mBCH (100,000 satoshis = 1 mBCH).
|
||||||
|
*
|
||||||
|
* @returns {number} The mBCH amount.
|
||||||
|
*/
|
||||||
|
public toMBCH(): number {
|
||||||
|
return Satoshis.roundToDigits(Number(this.satoshis) / 100_000, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the amount to BCH (100,000,000 satoshis = 1 BCH).
|
||||||
|
*
|
||||||
|
* @returns {number} The BCH amount.
|
||||||
|
*/
|
||||||
|
public toBCH(): number {
|
||||||
|
return Satoshis.roundToDigits(Number(this.satoshis) / 100_000_000, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
binToHex,
|
||||||
|
hexToBin,
|
||||||
|
encodeTransaction,
|
||||||
|
decodeTransaction,
|
||||||
|
isHex,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
import type { TransactionBCH } from '@bitauth/libauth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction Entity.
|
||||||
|
*/
|
||||||
|
export class Transaction {
|
||||||
|
constructor(public transaction: TransactionBCH) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Transaction Entity from the given byte data.
|
||||||
|
*
|
||||||
|
* @param bytes {Uint8Array} The raw byte data of the transaction.
|
||||||
|
*
|
||||||
|
* @throws {Error} If transaction data cannot be decoded.
|
||||||
|
*
|
||||||
|
* @returns {Transaction} The created Transaction Entity.
|
||||||
|
*/
|
||||||
|
public static fromBytes(bytes: Uint8Array): Transaction {
|
||||||
|
// Attempt to decode the raw transaction bytes.
|
||||||
|
const decodeResult = decodeTransaction(bytes);
|
||||||
|
|
||||||
|
// If a string is returned, this indicates an error...
|
||||||
|
if (typeof decodeResult === 'string') {
|
||||||
|
throw new Error(decodeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new instance of the Transaction Entity created from the decode result.
|
||||||
|
return new Transaction(decodeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Transaction Entity from the given byte data encoded as hexadecimal string.
|
||||||
|
*
|
||||||
|
* @param hex {string} The raw byte data (encoded as hexadecimal string) of the transaction.
|
||||||
|
*
|
||||||
|
* @throws {Error} If transaction cannot be decoded.
|
||||||
|
*
|
||||||
|
* @returns {Transaction} The created Transaction Entity.
|
||||||
|
*/
|
||||||
|
public static fromHex(hex: string): Transaction {
|
||||||
|
// Ensure that the given string is hex.
|
||||||
|
if (!isHex(hex)) {
|
||||||
|
throw new Error('The given string is not valid hexadecimal.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert our hex to binary.
|
||||||
|
const bytes = hexToBin(hex);
|
||||||
|
|
||||||
|
// Return the transaction constructed from our bytes.
|
||||||
|
return Transaction.fromBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toBytes(): Uint8Array {
|
||||||
|
const bytes = encodeTransaction(this.transaction);
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toHex(): string {
|
||||||
|
const bytes = this.toBytes();
|
||||||
|
|
||||||
|
return binToHex(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVersion() {
|
||||||
|
return this.transaction.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInputs() {
|
||||||
|
return this.transaction.inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOutputs() {
|
||||||
|
return this.transaction.outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLocktime() {
|
||||||
|
return this.transaction.locktime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
export class EventEmitter<
|
||||||
|
T extends Record<string, unknown>
|
||||||
|
> extends EventTarget {
|
||||||
|
private listeners: Map<keyof T, Set<EventListener>> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
on<K extends keyof T>(type: K, listener: (detail: T[K]) => void) {
|
||||||
|
const wrappedListener = ((e: CustomEvent<T[K]>) =>
|
||||||
|
listener(e.detail)) as EventListener;
|
||||||
|
this.addEventListener(type as string, wrappedListener);
|
||||||
|
|
||||||
|
if (!this.listeners.has(type)) {
|
||||||
|
this.listeners.set(type, new Set());
|
||||||
|
}
|
||||||
|
this.listeners.get(type)?.add(wrappedListener);
|
||||||
|
|
||||||
|
return wrappedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
off<K extends keyof T>(type: K, listener: EventListener) {
|
||||||
|
this.removeEventListener(type as string, listener);
|
||||||
|
this.listeners.get(type)?.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<K extends keyof T>(type: K, detail: T[K]) {
|
||||||
|
const event = new CustomEvent(type as string, { detail });
|
||||||
|
return this.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllListeners() {
|
||||||
|
this.listeners.forEach((listeners, type) => {
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
this.removeEventListener(type as string, listener);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.listeners.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
monitorProperty<ObjectType, Property extends keyof ObjectType>(
|
||||||
|
target: ObjectType,
|
||||||
|
key: Property,
|
||||||
|
eventName: keyof T
|
||||||
|
) {
|
||||||
|
let value = target[key];
|
||||||
|
|
||||||
|
Object.defineProperty(target, key, {
|
||||||
|
get: () => {
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
set: (newValue) => {
|
||||||
|
value = newValue;
|
||||||
|
this.emit(eventName as string, newValue);
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './event-emitter.js';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './wallet-hd-vue.js';
|
||||||
|
export * from './wallet-p2pkh-vue.js';
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ElectrumService } from 'src/services/electrum.js';
|
||||||
|
import { WalletHD } from 'src/libcash/wallet/index.js';
|
||||||
|
import { WalletP2PKHVue, useWalletP2PKHVue } from './wallet-p2pkh-vue.js';
|
||||||
|
|
||||||
|
import { computed, ref, shallowRef } from 'vue';
|
||||||
|
|
||||||
|
import { HdPrivateNodeValid } from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export class WalletHDVue extends WalletHD<WalletP2PKHVue> {
|
||||||
|
// Reactive State.
|
||||||
|
public rShouldMonitor = ref(false);
|
||||||
|
public rWallets = shallowRef<Array<WalletP2PKHVue>>([]);
|
||||||
|
|
||||||
|
// Computeds.
|
||||||
|
public rBalance = computed(() => {
|
||||||
|
return this.rWallets.value.reduce(
|
||||||
|
(total, node) => total + node.rBalance.value,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
node: HdPrivateNodeValid,
|
||||||
|
electrum: ElectrumService,
|
||||||
|
walletFactory = useWalletP2PKHVue
|
||||||
|
) {
|
||||||
|
super(node, electrum, walletFactory);
|
||||||
|
|
||||||
|
this.events.on(
|
||||||
|
'walletsUpdated',
|
||||||
|
(wallets) => (this.rWallets.value = wallets)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.events.on(
|
||||||
|
'shouldMonitorUpdated',
|
||||||
|
(shouldMonitor) => (this.rShouldMonitor.value = shouldMonitor)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { WalletP2PKH } from 'src/libcash/wallet/index.js';
|
||||||
|
|
||||||
|
import { type AddressListUnspent } from 'src/services/electrum-types';
|
||||||
|
import { ElectrumService } from 'src/services/electrum.js';
|
||||||
|
import { Transaction } from 'src/libcash/primitives/index.js';
|
||||||
|
|
||||||
|
import { computed, shallowRef } from 'vue';
|
||||||
|
|
||||||
|
export class WalletP2PKHVue extends WalletP2PKH {
|
||||||
|
// Reactive State.
|
||||||
|
public rUnspents = shallowRef<AddressListUnspent['response']>([]);
|
||||||
|
public rTransactions = shallowRef<Array<Transaction>>([]);
|
||||||
|
|
||||||
|
// Computeds.
|
||||||
|
public rBalance = computed(() => {
|
||||||
|
return this.rUnspents.value.reduce(
|
||||||
|
(total, unspent) => total + unspent.value,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(privateKeyBytes: Uint8Array, electrum: ElectrumService) {
|
||||||
|
super(privateKeyBytes, electrum);
|
||||||
|
|
||||||
|
this.events.on('unspentsUpdated', (unspents) => {
|
||||||
|
this.rUnspents.value = unspents;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.events.on('transactionsUpdated', (transactions) => {
|
||||||
|
this.rTransactions.value = transactions;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWalletP2PKHVue(
|
||||||
|
privateKeyBytes: Uint8Array,
|
||||||
|
electrum: ElectrumService
|
||||||
|
): WalletP2PKHVue {
|
||||||
|
return new WalletP2PKHVue(privateKeyBytes, electrum);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ElectrumService } from 'src/services/electrum.js';
|
||||||
|
|
||||||
|
export type ServiceConfig<BlockchainType, WalletType> = {
|
||||||
|
createBlockchain: () => BlockchainType;
|
||||||
|
createWallet: (
|
||||||
|
privateKeyBytes: Uint8Array,
|
||||||
|
electrum: ElectrumService
|
||||||
|
) => WalletType;
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './wallet-hd.js';
|
||||||
|
export * from './wallet-p2pkh.js';
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { PrivateKey, Transaction } from 'src/libcash/primitives/index.js';
|
||||||
|
import { EventEmitter } from 'src/libcash/utils/index.js';
|
||||||
|
import { type AddressListUnspent } from 'src/services/electrum-types';
|
||||||
|
import { ElectrumService } from 'src/services/electrum';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CompilerBCH,
|
||||||
|
type CompilationData,
|
||||||
|
type InputTemplate,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export type InputTemplateBCH = InputTemplate<
|
||||||
|
CompilerBCH,
|
||||||
|
false,
|
||||||
|
CompilationData
|
||||||
|
>;
|
||||||
|
|
||||||
|
export abstract class WalletBase extends PrivateKey {
|
||||||
|
// Dependencies/Services
|
||||||
|
public electrum: ElectrumService;
|
||||||
|
|
||||||
|
constructor(privateKeyBytes: Uint8Array, electrum: ElectrumService) {
|
||||||
|
super(privateKeyBytes);
|
||||||
|
this.electrum = electrum;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract startMonitoring(): Promise<void>;
|
||||||
|
|
||||||
|
abstract stopMonitoring(): Promise<void>;
|
||||||
|
|
||||||
|
abstract getAddress(): string;
|
||||||
|
|
||||||
|
abstract fetchHistory(): Promise<Array<Transaction>>;
|
||||||
|
|
||||||
|
abstract fetchUnspentOutputs(): Promise<AddressListUnspent['response']>;
|
||||||
|
|
||||||
|
abstract getUnspentDirectives(): Promise<Array<InputTemplateBCH>>;
|
||||||
|
|
||||||
|
async refresh(): Promise<void> {
|
||||||
|
await Promise.all([this.fetchUnspentOutputs(), this.fetchHistory()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onAddressNotification(status: string | null) {
|
||||||
|
// If status is null, it simply means that our subscribe call to Electrum was successful.
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh our wallet's state.
|
||||||
|
await this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
// Import the Stamp class to derive the private keys
|
||||||
|
import { HDPrivateNode } from 'src/libcash/primitives/index.js';
|
||||||
|
import { EventEmitter } from 'src/libcash/utils/index.js';
|
||||||
|
import { ElectrumService } from 'src/services/electrum';
|
||||||
|
import {
|
||||||
|
WalletP2PKH,
|
||||||
|
WalletP2PKHFactory,
|
||||||
|
useWalletP2PKH,
|
||||||
|
} from './wallet-p2pkh.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type HdPrivateNodeValid,
|
||||||
|
deriveSeedFromBip39Mnemonic,
|
||||||
|
encodeTransaction,
|
||||||
|
generateTransaction,
|
||||||
|
getMinimumFee,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export const DERIVATION_PATH = "m/44'/145'/0'";
|
||||||
|
export const ADDRESS_GAP = 20;
|
||||||
|
|
||||||
|
export type WalletHDEvents<WalletType> = {
|
||||||
|
shouldMonitorUpdated: boolean;
|
||||||
|
walletsUpdated: Array<WalletType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WalletHD<
|
||||||
|
WalletType extends WalletP2PKH = WalletP2PKH
|
||||||
|
> extends HDPrivateNode {
|
||||||
|
// Events.
|
||||||
|
public events: EventEmitter<WalletHDEvents<WalletType>> = new EventEmitter();
|
||||||
|
|
||||||
|
// Mutable State.
|
||||||
|
public shouldMonitor = false;
|
||||||
|
public wallets: Array<WalletType> = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public node: HdPrivateNodeValid,
|
||||||
|
public electrum: ElectrumService,
|
||||||
|
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH
|
||||||
|
) {
|
||||||
|
// Instantiate parent class.
|
||||||
|
super(node);
|
||||||
|
|
||||||
|
// Monitor the following properties and emit an event when they change.
|
||||||
|
this.events.monitorProperty(this, 'shouldMonitor', 'shouldMonitorUpdated');
|
||||||
|
this.events.monitorProperty(this, 'wallets', 'walletsUpdated');
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromMnemonic<T extends WalletHD<any>>(
|
||||||
|
this: WalletHDConstructor<T>,
|
||||||
|
mnemonic: string,
|
||||||
|
electrum: ElectrumService,
|
||||||
|
walletFactory?: WalletP2PKHFactory<any>
|
||||||
|
): T {
|
||||||
|
// Derive the seed from the mnemonic.
|
||||||
|
const seed = deriveSeedFromBip39Mnemonic(mnemonic);
|
||||||
|
|
||||||
|
// Derive node from mnemonic.
|
||||||
|
const hdPrivateNode = HDPrivateNode.fromSeed(seed);
|
||||||
|
|
||||||
|
// Return a new instance of this class.
|
||||||
|
return new this(hdPrivateNode.node, electrum, walletFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
this.events.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setQuantity(quantity: number) {
|
||||||
|
// Clear existing nodes.
|
||||||
|
this.wallets = this.deriveWallets(quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async startMonitoring() {
|
||||||
|
this.shouldMonitor = true;
|
||||||
|
|
||||||
|
await Promise.all(this.wallets.map((wallet) => wallet.startMonitoring()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopMonitoring() {
|
||||||
|
this.shouldMonitor = false;
|
||||||
|
|
||||||
|
await Promise.all(this.wallets.map((wallet) => wallet.stopMonitoring()));
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveWallets(count: number, startIndex = 0) {
|
||||||
|
// Create an array to store our wallets.
|
||||||
|
const wallets: Array<WalletType> = [];
|
||||||
|
|
||||||
|
// Create the given count of wallets.
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const childNode = this.derivePath(
|
||||||
|
`${DERIVATION_PATH}/0/${startIndex + i}`
|
||||||
|
);
|
||||||
|
const wallet = this.walletFactory(
|
||||||
|
childNode.privateKey().toBytes(),
|
||||||
|
this.electrum
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.shouldMonitor) {
|
||||||
|
wallet.startMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
wallets.push(wallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wallets = wallets;
|
||||||
|
|
||||||
|
// Return the wallets.
|
||||||
|
return wallets;
|
||||||
|
}
|
||||||
|
|
||||||
|
async scan(startIndex = 0, addressGap = 20) {
|
||||||
|
// Array to store active wallet nodes
|
||||||
|
const nodes: Array<WalletType> = [];
|
||||||
|
|
||||||
|
let currentIndex = startIndex;
|
||||||
|
let emptyAddressCount = 0;
|
||||||
|
|
||||||
|
// Continue scanning until we find 'addressGap' consecutive empty addresses
|
||||||
|
while (emptyAddressCount < addressGap) {
|
||||||
|
// Derive a number of wallets equivalent to our addressGap.
|
||||||
|
const batch: Array<WalletType> = this.deriveWallets(
|
||||||
|
addressGap,
|
||||||
|
currentIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the transaction history of each wallet in the batch
|
||||||
|
const results = await Promise.all(
|
||||||
|
batch.map((wallet) => wallet.fetchHistory())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process the results
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
if (results[i].length > 0) {
|
||||||
|
// This address has transactions, add it to nodes and reset empty address count
|
||||||
|
nodes.push(batch[i]);
|
||||||
|
emptyAddressCount = 0;
|
||||||
|
} else {
|
||||||
|
// This address is empty, increment the empty address count
|
||||||
|
emptyAddressCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex++;
|
||||||
|
|
||||||
|
// If we've found 'addressGap' consecutive empty addresses, stop scanning
|
||||||
|
if (emptyAddressCount >= addressGap) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wallets = nodes;
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshChildNodes() {
|
||||||
|
await Promise.all(this.wallets.map((node) => node.refresh()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async sweep(payoutBytecode: Uint8Array) {
|
||||||
|
// Get a list of inputs belonging to each wallet.
|
||||||
|
const inputs = await Promise.all(
|
||||||
|
this.wallets.map((wallet) => wallet.getUnspentDirectives())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flatten the inputs.
|
||||||
|
const inputsFlattened = inputs.flat();
|
||||||
|
|
||||||
|
// Calculate the total sats available in our inputs.
|
||||||
|
const totalSats = inputsFlattened.reduce((total, input) => {
|
||||||
|
if (input.unlockingBytecode instanceof Uint8Array) {
|
||||||
|
return (total += 0n);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (total += input.unlockingBytecode.valueSatoshis);
|
||||||
|
}, 0n);
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO: Determine the amounts and categories of any tokens in the inputs.
|
||||||
|
const tokens = inputsFlattened.reduce((tokens, input) => {
|
||||||
|
// Improve legibility by getting the token.
|
||||||
|
const token = input.unlockingBytecode.token;
|
||||||
|
|
||||||
|
if(!token) {
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens[token.
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}, {});
|
||||||
|
*/
|
||||||
|
// TODO: Add an output for each token.
|
||||||
|
|
||||||
|
// We need to calculate the number of bytes so that we can calculate the fee.
|
||||||
|
// So we loop twice and store the final transaction here each time.
|
||||||
|
// 1st time will have zero fee. 2nd time will accommodate the fee.
|
||||||
|
let encodedTransaction = new Uint8Array();
|
||||||
|
|
||||||
|
// Create the transaction by looping twice.
|
||||||
|
// 1st loop: Transaction without a fee.
|
||||||
|
// 2nd loop: Accommodate the fee.
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
// Get the fee using 1000 sats/KB.
|
||||||
|
const feeSats = getMinimumFee(BigInt(encodedTransaction.length), 1000n);
|
||||||
|
|
||||||
|
// Attempt to generate the transaction.
|
||||||
|
const generatedTransaction = generateTransaction({
|
||||||
|
version: 2,
|
||||||
|
locktime: 0,
|
||||||
|
inputs: inputsFlattened,
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
lockingBytecode: payoutBytecode,
|
||||||
|
valueSatoshis: totalSats - feeSats,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!generatedTransaction.success) {
|
||||||
|
console.error(generatedTransaction.errors);
|
||||||
|
|
||||||
|
throw new Error('Failed to generate transaction');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the transaction for broadcasting (and fee estimation).
|
||||||
|
encodedTransaction = encodeTransaction(generatedTransaction.transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodedTransaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Factories Types/Functions
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type WalletHDParams = ConstructorParameters<typeof WalletHD>;
|
||||||
|
export type WalletHDConstructor<T> = new (...args: WalletHDParams) => T;
|
||||||
|
export type WalletHDFactory<T extends WalletHD = WalletHD> = (
|
||||||
|
...args: WalletHDParams
|
||||||
|
) => T;
|
||||||
|
|
||||||
|
export function useWalletHD<T extends WalletHD = WalletHD>(
|
||||||
|
...args: WalletHDParams
|
||||||
|
): T {
|
||||||
|
return new WalletHD(...args) as T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { Transaction } from 'src/libcash/primitives/index.js';
|
||||||
|
import { EventEmitter } from 'src/libcash/utils/index.js';
|
||||||
|
import {
|
||||||
|
type AddressListUnspent,
|
||||||
|
type AddressGetHistory,
|
||||||
|
type TransactionGet,
|
||||||
|
} from 'src/services/electrum-types';
|
||||||
|
import { ElectrumService } from 'src/services/electrum';
|
||||||
|
|
||||||
|
import { WalletBase } from './wallet-base.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
hexToBin,
|
||||||
|
walletTemplateP2pkhNonHd,
|
||||||
|
walletTemplateToCompilerBCH,
|
||||||
|
} from '@bitauth/libauth';
|
||||||
|
|
||||||
|
export type WalletP2PKHEvents = {
|
||||||
|
transactionsUpdated: Array<Transaction>;
|
||||||
|
unspentsUpdated: AddressListUnspent['response'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WalletP2PKH extends WalletBase {
|
||||||
|
// Events.
|
||||||
|
public events: EventEmitter<WalletP2PKHEvents> = new EventEmitter();
|
||||||
|
|
||||||
|
// State.
|
||||||
|
public transactions: Array<Transaction> = [];
|
||||||
|
public unspents: AddressListUnspent['response'] = [];
|
||||||
|
|
||||||
|
constructor(privateKeyBytes: Uint8Array, electrum: ElectrumService) {
|
||||||
|
super(privateKeyBytes, electrum);
|
||||||
|
|
||||||
|
// Monitor the following properties and emit an event when they change.
|
||||||
|
this.events.monitorProperty(this, 'transactions', 'transactionsUpdated');
|
||||||
|
this.events.monitorProperty(this, 'unspents', 'unspentsUpdated');
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
this.events.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
async startMonitoring() {
|
||||||
|
await this.electrum.subscribeAddress(
|
||||||
|
this.getAddress(),
|
||||||
|
this.onAddressNotification.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopMonitoring() {
|
||||||
|
await this.electrum.unsubscribeAddress(
|
||||||
|
this.getAddress(),
|
||||||
|
this.onAddressNotification.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddress() {
|
||||||
|
return this.derivePublicKey().deriveAddress().toCashAddr();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchHistory() {
|
||||||
|
// Get this node's address.
|
||||||
|
const address = this.getAddress();
|
||||||
|
|
||||||
|
// Get the history of the address from electrum
|
||||||
|
const history = await this.electrum.request<AddressGetHistory>(
|
||||||
|
'blockchain.address.get_history',
|
||||||
|
address,
|
||||||
|
0,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the transactions for each history item.
|
||||||
|
const transactions = await Promise.all(
|
||||||
|
history.map((item) =>
|
||||||
|
this.electrum.request<TransactionGet>(
|
||||||
|
'blockchain.transaction.get',
|
||||||
|
item.tx_hash,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decode the transactions.
|
||||||
|
const decodedTransactions = transactions.map((tx) =>
|
||||||
|
Transaction.fromHex(tx)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the transactions.
|
||||||
|
this.transactions = decodedTransactions;
|
||||||
|
|
||||||
|
return decodedTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUnspentOutputs() {
|
||||||
|
const address = this.getAddress();
|
||||||
|
|
||||||
|
const unspentOutputs = await this.electrum.request<AddressListUnspent>(
|
||||||
|
'blockchain.address.listunspent',
|
||||||
|
address,
|
||||||
|
'include_tokens'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Emit event with unspent outputs.
|
||||||
|
this.unspents = unspentOutputs;
|
||||||
|
|
||||||
|
return unspentOutputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUnspentDirectives() {
|
||||||
|
// Get a list of unspent outputs.
|
||||||
|
const unspentOutputs = await this.fetchUnspentOutputs();
|
||||||
|
|
||||||
|
// Create our P2PKH Compiler.
|
||||||
|
const compilerP2PKH = walletTemplateToCompilerBCH(walletTemplateP2pkhNonHd);
|
||||||
|
|
||||||
|
// Transform our inputs into unlocking directives.
|
||||||
|
const inputDirectives = unspentOutputs.map((unspent) => ({
|
||||||
|
outpointIndex: unspent.tx_pos,
|
||||||
|
outpointTransactionHash: hexToBin(unspent.tx_hash),
|
||||||
|
sequenceNumber: 0,
|
||||||
|
unlockingBytecode: {
|
||||||
|
compiler: compilerP2PKH,
|
||||||
|
data: {
|
||||||
|
keys: { privateKeys: { key: this.toBytes() } },
|
||||||
|
},
|
||||||
|
script: 'unlock',
|
||||||
|
token: unspent.token_data
|
||||||
|
? {
|
||||||
|
amount: BigInt(unspent.token_data.amount),
|
||||||
|
category: hexToBin(unspent.token_data.category),
|
||||||
|
nft: unspent.token_data.nft
|
||||||
|
? {
|
||||||
|
capability: unspent.token_data.nft.capability,
|
||||||
|
commitment: hexToBin(unspent.token_data.nft.commitment),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
valueSatoshis: BigInt(unspent.value),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Return the directives.
|
||||||
|
return inputDirectives;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(): Promise<void> {
|
||||||
|
await Promise.all([this.fetchUnspentOutputs(), this.fetchHistory()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onAddressNotification(status: string | null) {
|
||||||
|
// If status is null, it simply means that our subscribe call to Electrum was successful.
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh our wallet's state.
|
||||||
|
await this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WalletP2PKHFactory<T extends WalletP2PKH = WalletP2PKH> = (
|
||||||
|
privateKeyBytes: Uint8Array,
|
||||||
|
electrum: ElectrumService
|
||||||
|
) => T;
|
||||||
|
|
||||||
|
export function useWalletP2PKH<T extends WalletP2PKH = WalletP2PKH>(
|
||||||
|
privateKeyBytes: Uint8Array,
|
||||||
|
electrum: ElectrumService
|
||||||
|
): T {
|
||||||
|
return new WalletP2PKH(privateKeyBytes, electrum) as T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Target esnext (rollup step will transpile to CJS and EJS)
|
||||||
|
"target": "esnext",
|
||||||
|
|
||||||
|
// Use node as module resolution strategy for best compatibility with dependencies
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "esnext",
|
||||||
|
|
||||||
|
// Set the root directory and output directory
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./build/lib",
|
||||||
|
|
||||||
|
// Allow ES modules to be used
|
||||||
|
"esModuleInterop": true,
|
||||||
|
|
||||||
|
// Allow importing .json files in project
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
|
// Generate declarations, declaration maps and sourcemaps
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
|
||||||
|
// Use strict Typescript rules
|
||||||
|
"strict": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
|
||||||
|
// Allow use of ES2017 and DOM features/libraries
|
||||||
|
"lib": ["es2017", "dom"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Exclude these directories from the Typescript transpile
|
||||||
|
"exclude": ["node_modules/**", "dist/**", "public/**"],
|
||||||
|
|
||||||
|
// Do not compile upon save - this feature requires tsc-w(atch)
|
||||||
|
"compileOnSave": true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user