PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-stx/src/lib
Просмотр файла: utils.ts
import * as url from 'url';
import BigNumber from 'bignumber.js';
import { bufferToHex, stripHexPrefix } from 'ethereumjs-util';
import {
addressFromPublicKeys,
addressFromVersionHash,
AddressHashMode,
addressHashModeToVersion,
addressToString,
AddressVersion,
BufferReader,
ClarityType,
ClarityValue,
createAddress,
createMemoString,
createMessageSignature,
createStacksPrivateKey,
createStacksPublicKey,
cvToString,
cvToValue,
deserializeTransaction,
PubKeyEncoding,
publicKeyFromSignature,
signWithKey,
StacksTransaction,
TransactionVersion,
validateStacksAddress,
} from '@stacks/transactions';
import { secp256k1 } from '@noble/curves/secp256k1';
import * as _ from 'lodash';
import { InvalidTransactionError, isValidXprv, isValidXpub, SigningError, UtilsError } from '@bitgo/sdk-core';
import { AddressDetails, SendParams, TokenTransferParams } from './iface';
import { KeyPair } from '.';
import { coins, Sip10Token, StacksNetwork as BitgoStacksNetwork } from '@bitgo/statics';
import { VALID_CONTRACT_FUNCTION_NAMES } from './constants';
/**
* Encodes a buffer as a "0x" prefixed lower-case hex string.
*
* @param {Buffer} buff - a buffer with a hexadecimal string
* @returns {string} - the hexadecimal string prefixed with "0x"
*/
export function bufferToHexPrefixString(buff: Buffer): string {
return bufferToHex(buff);
}
/**
* Remove the "0x" prefix from the given string, if present.
*
* @param {string} hex - a hexadecimal string
* @returns {string} - the hexadecimal string without a leading "0x"
*/
export function removeHexPrefix(hex: string): string {
return stripHexPrefix(hex);
}
/**
* Get stacks address from public key hash
*
* @param {Buffer} publicKeyHash - hash of public key
* @param {AddressHashMode} hashMode - hash mode
* @param {TransactionVersion} transactionVersion - tx version
* @returns {string} stacks address
*/
function getAddressFromPublicKeyHash(
publicKeyHash: Buffer,
hashMode: AddressHashMode,
transactionVersion: TransactionVersion
): string {
if (publicKeyHash.length !== 20) {
throw new Error('expected 20-byte pubkeyhash');
}
const addrVer = addressHashModeToVersion(hashMode, transactionVersion);
const addr = addressFromVersionHash(addrVer, publicKeyHash.toString('hex'));
const addrString = addressToString(addr);
return addrString;
}
/**
* @param tx
*/
export function getTxSenderAddress(tx: StacksTransaction): string {
if (tx.auth.spendingCondition !== null && tx.auth.spendingCondition !== undefined) {
const spendingCondition = tx.auth.spendingCondition;
const txSender = getAddressFromPublicKeyHash(
Buffer.from(spendingCondition.signer, 'hex'),
spendingCondition.hashMode as number,
tx.version
);
return txSender;
} else throw new Error('spendingCondition should not be null');
}
/**
* Returns whether or not the string is a valid amount number
*
* @param {string} amount - the string to validate
* @returns {boolean} - the validation result
*/
export function isValidAmount(amount: string): boolean {
const bigNumberAmount = new BigNumber(amount);
return bigNumberAmount.isInteger() && bigNumberAmount.isGreaterThanOrEqualTo(0);
}
/**
* Returns whether or not the string is a valid protocol address
*
* @param {string} address - the address to be validated
* @returns {boolean} - the validation result
*/
export function isValidAddress(address: string): boolean {
return validateStacksAddress(address);
}
/**
* Returns whether or not the string is a valid protocol transaction id or not.
*
* A valid transaction id is a SHA-512/256 hash of a serialized transaction; see
* the txidFromData function in @stacks/transaction:
* https://github.com/blockstack/stacks.js/blob/master/packages/transactions/src/utils.ts#L97
*
* @param {string} txId - the transaction id to be validated
* @returns {boolean} - the validation result
*/
export function isValidTransactionId(txId: string): boolean {
if (txId.length !== 64 && txId.length !== 66) return false;
const noPrefix = removeHexPrefix(txId);
if (noPrefix.length !== 64) return false;
return allHexChars(noPrefix);
}
/**
* Returns whether or not the string is a valid protocol public key or
* extended public key.
*
* The key format is documented at
* https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md#transaction-authorization
*
* @param {string} pub - the public key to be validated
* @returns {boolean} - the validation result
*/
export function isValidPublicKey(pub: string): boolean {
if (isValidXpub(pub)) return true;
if (pub.length !== 66 && pub.length !== 130) return false;
const firstByte = pub.slice(0, 2);
// uncompressed public key
if (pub.length === 130 && firstByte !== '04') return false;
// compressed public key
if (pub.length === 66 && firstByte !== '02' && firstByte !== '03') return false;
if (!allHexChars(pub)) return false;
// validate the public key
try {
secp256k1.ProjectivePoint.fromHex(pub);
return true;
} catch (e) {
return false;
}
}
/**
* Returns whether or not the string is a valid protocol private key, or extended
* private key.
*
* The protocol key format is described in the @stacks/transactions npm package, in the
* createStacksPrivateKey function:
* https://github.com/blockstack/stacks.js/blob/master/packages/transactions/src/keys.ts#L125
*
* @param {string} prv - the private key (or extended private key) to be validated
* @returns {boolean} - the validation result
*/
export function isValidPrivateKey(prv: string): boolean {
if (isValidXprv(prv)) return true;
if (prv.length !== 64 && prv.length !== 66) return false;
if (prv.length === 66 && prv.slice(64) !== '01') return false;
return allHexChars(prv);
}
/**
* Returns whether or not the string is a composed of hex chars only
*
* @param {string} maybe - the string to be validated
* @returns {boolean} - the validation result
*/
function allHexChars(maybe: string): boolean {
return /^([0-9a-f])+$/i.test(maybe);
}
/**
* Checks if raw transaction can be deserialized
*
* @param {unknown} rawTransaction - transaction in raw hex format
* @returns {boolean} - the validation result
*/
export function isValidRawTransaction(rawTransaction: unknown): boolean {
try {
if (typeof rawTransaction === 'string') {
deserializeTransaction(BufferReader.fromBuffer(Buffer.from(removeHexPrefix(rawTransaction), 'hex')));
} else {
return false;
}
} catch (e) {
return false;
}
return true;
}
/**
* Returns whether or not the memo string is valid
*
* @param {string} memo - the string to be validated
* @returns {boolean} - the validation result
*/
export function isValidMemo(memo: string): boolean {
try {
createMemoString(memo);
} catch (e) {
return false;
}
return true;
}
/**
* Checks for valid contract address
*
* @param {string} addr - contract deployer address
* @param {BitgoStacksNetwork} network - network object
* @returns {boolean} - the validation result
*/
export function isValidContractAddress(addr: string, network: BitgoStacksNetwork): boolean {
return addr === network.stakingContractAddress || addr === network.sendmanymemoContractAddress;
}
/**
* Check if the name is one of valid contract names
*
* @param {string} name - function name
* @returns {boolean} - validation result
*/
export function isValidContractFunctionName(name: string): boolean {
return VALID_CONTRACT_FUNCTION_NAMES.includes(name);
}
/**
* Unpads a memo string, so it removes nulls.
*
* Useful when memo is fill up the length. Result is becomes readable.
*
* @param {string} memo - the string to be validated
* @returns {boolean} - the validation result
*/
export function unpadMemo(memo: string): string {
const end = memo.indexOf('\u0000');
if (end < 0) return memo;
return memo.slice(0, end);
}
/**
* Generate a multisig address from multiple STX public keys
*
* @param {string[]} pubKeys - list of public keys as strings
* @param {AddressVersion} addressVersion - MainnetMultiSig, TestnetMultiSig
* @param {AddressHashMode} addressHashMode - SerializeP2SH
* @param {number} [signaturesRequired] - number of signatures required, default value its 2
* @returns {address: string, hash160: string} - a multisig address
*/
export function getSTXAddressFromPubKeys(
pubKeys: string[],
addressVersion: AddressVersion = AddressVersion.MainnetMultiSig,
addressHashMode: AddressHashMode = AddressHashMode.SerializeP2SH,
signaturesRequired = 2
): { address: string; hash160: string } {
if (pubKeys.length === 0) {
throw new Error('Invalid number of public keys');
}
if (!pubKeys.every(isValidPublicKey)) {
throw new Error('Invalid public keys');
}
if (signaturesRequired > pubKeys.length) {
throw new Error('Number of signatures required must be lower or equal to the number of Public Keys');
}
const stxPubKeys = pubKeys.map(createStacksPublicKey);
const address = addressFromPublicKeys(addressVersion, addressHashMode, signaturesRequired, stxPubKeys);
return { address: addressToString(address), hash160: address.hash160 };
}
/**
* signs a string message
*
* @param keyPair
* @param data - message to be signed
* @returns signed message string
*/
export function signMessage(keyPair: KeyPair, data: string): string {
const prv = keyPair.getKeys().prv;
if (prv) {
return signWithKey(createStacksPrivateKey(prv), data).data;
} else {
throw new SigningError('Missing private key');
}
}
/**
* Verifies a signed message
*
* The signature must be 130 bytes long -- see RECOVERABLE_ECDSA_SIG_LENGTH_BYTES
* in @stacks/transactions/src/constants.ts
*
* @param {string} message - message to verify the signature
* @param {string} signature - signature to verify
* @param {string} publicKey - public key as hex string used to verify the signature
* @returns {boolean} - verification result
*/
export function verifySignature(message: string, signature: string, publicKey: string): boolean {
if (!isValidPublicKey(publicKey)) return false;
if (signature.length !== 130) return false;
if (!allHexChars(signature)) throw new UtilsError('Invalid signature input to verifySignature');
if (_.isEmpty(message)) throw new UtilsError('Cannot verify empty messages');
// provided publicKey can be compressed or uncompressed
const keyEncoding = publicKey.length === 66 ? PubKeyEncoding.Compressed : PubKeyEncoding.Uncompressed;
const messageSig = createMessageSignature(signature);
const foundKey = publicKeyFromSignature(message, messageSig, keyEncoding);
return foundKey === publicKey;
}
/**
* Process address into address and memo id
*
* @param {string} address the address to process
* @returns {Object} object containing address and memo id
*/
export function getAddressDetails(address: string): AddressDetails {
const addressDetails = url.parse(address);
const queryDetails = addressDetails.query ? new URLSearchParams(addressDetails.query) : undefined;
const baseAddress = addressDetails.pathname as string;
if (!isValidAddress(baseAddress)) {
throw new UtilsError(`invalid address: ${address}`);
}
// address doesn't have a memo id
if (baseAddress === address) {
return {
address: address,
memoId: undefined,
};
}
if (!queryDetails || _.isNil(queryDetails.get('memoId'))) {
// if there are more properties, the query details need to contain the memo id property
throw new UtilsError(`invalid address with memo id: ${address}`);
}
const memoId = queryDetails.get('memoId') as string;
const intMemoId = parseInt(memoId, 10);
if (isNaN(intMemoId) || intMemoId < 0) {
throw new Error(`invalid memo id: ${memoId}`);
}
return {
address: baseAddress,
memoId,
};
}
/**
* Validate and return address with appended memo id
*
* @param {AddressDetails} addressDetails
* @returns {string} address with memo id
*/
export function normalizeAddress({ address, memoId }: AddressDetails): string {
if (!isValidAddress(address)) {
throw new UtilsError(`invalid address: ${address}`);
}
if (!_.isUndefined(memoId)) {
const intMemoId = parseInt(memoId, 10);
if (isNaN(intMemoId) || intMemoId < 0) {
throw new Error(`invalid memo id: ${memoId}`);
}
return `${address}?memoId=${memoId}`;
}
return address;
}
/**
* Return boolean indicating whether input is a valid address with memo id
*
* @param {string} address address in the form <address>?memoId=<memoId>
* @returns {boolean} true is input is a valid address
*/
export function isValidAddressWithPaymentId(address: string): boolean {
try {
const addressDetails = getAddressDetails(address);
return address === normalizeAddress(addressDetails);
} catch (e) {
return false;
}
}
/**
* Return string representation of clarity value input
*
* @param {ClarityValue} cv clarity value function argument
* @returns {String} stringified clarity value
*/
export function stringifyCv(cv: ClarityValue): any {
switch (cv.type) {
case ClarityType.Int:
case ClarityType.UInt:
return { type: cv.type, value: cv.value.toString() };
case ClarityType.OptionalSome:
return { type: cv.type, value: stringifyCv(cv.value) };
case ClarityType.Tuple:
return {
type: cv.type,
data: _.mapValues(cv.data, (value) => stringifyCv(value)),
};
case ClarityType.List:
return {
type: cv.type,
list: cv.list.map(stringifyCv),
};
default:
return cv;
}
}
/**
* Parse functionArgs into send params for send-many-memo contract calls
*
* @param {ClarityValue[]} args functionArgs from a contract call payload
* @returns {SendParams[]} An array of sendParams
*/
export function functionArgsToSendParams(args: ClarityValue[]): SendParams[] {
if (args.length !== 1 || args[0].type !== ClarityType.List) {
throw new InvalidTransactionError("function args don't match send-many-memo type declaration");
}
return args[0].list.map((tuple) => {
if (
tuple.type !== ClarityType.Tuple ||
tuple.data.to?.type !== ClarityType.PrincipalStandard ||
tuple.data.ustx?.type !== ClarityType.UInt ||
tuple.data.memo?.type !== ClarityType.Buffer
) {
throw new InvalidTransactionError("function args don't match send-many-memo type declaration");
}
return {
address: cvToString(tuple.data.to),
amount: cvToValue(tuple.data.ustx, true),
memo: tuple.data.memo.buffer.toString('ascii'),
};
});
}
export function functionArgsToTokenTransferParams(args: ClarityValue[]): TokenTransferParams {
if (args.length < 3) {
throw new InvalidTransactionError("function args don't match token transfer declaration");
}
if (
args[0].type !== ClarityType.UInt ||
args[1].type !== ClarityType.PrincipalStandard ||
args[2].type !== ClarityType.PrincipalStandard
) {
throw new InvalidTransactionError("function args don't match token transfer declaration");
}
const tokenTransferParams = {
amount: cvToValue(args[0], true),
sender: cvToString(args[1]),
recipient: cvToString(args[2]),
};
if (args.length === 4 && args[3].type === ClarityType.Buffer) {
tokenTransferParams['memo'] = args[3].buffer.toString('ascii');
}
return tokenTransferParams;
}
/**
* Gets the version of an address
*
* @param {String} address the address with or without the memoId
* @returns {AddressVersion} A number that represent the Address Version
*/
export function getAddressVersion(address: string): AddressVersion {
const baseAddress = getAddressDetails(address).address;
return createAddress(baseAddress).version;
}
/**
* Returns a STX pub key from an xpub
*
* @param {String} xpub an xpub
* @returns {String} a compressed STX pub key
*/
export function xpubToSTXPubkey(xpub: string, compressed = true): string {
return new KeyPair({ pub: xpub }).getKeys(compressed).pub;
}
/**
* Returns the base address portion of an address
*
* @param {String} address - an address
* @returns {String} - the base address
*/
export function getBaseAddress(address: string): string {
const addressDetails = getAddressDetails(address);
return addressDetails.address;
}
/**
* Compares an address to the base address to check if matchs.
*
* @param {String} address - an address
* @param {String} baseAddress - a base address
* @returns {boolean}
*/
export function isSameBaseAddress(address: string, baseAddress: string): boolean {
if (!isValidAddressWithPaymentId(address)) {
throw new UtilsError(`invalid address: ${address}`);
}
return getBaseAddress(address) === getBaseAddress(baseAddress);
}
/**
* Function to get tokenName from list of sip10 tokens using contract details
*
* @param {String} contractAddress
* @param {String} contractName
* @returns {String|Undefined}
*/
export function findTokenNameByContract(contractAddress: string, contractName: string): string | undefined {
{
const tokenName = coins
.filter((coin) => coin instanceof Sip10Token && coin.assetId.includes(`${contractAddress}.${contractName}`))
.map((coin) => coin.name);
return tokenName ? tokenName[0] : undefined;
}
}
/**
* Function to get contractTokenName from list of sip10 tokens using contract details
*
* @param {String} contractAddress
* @param {String} contractName
* @returns {String|Undefined}
*/
export function findContractTokenNameUsingContract(contractAddress: string, contractName: string): string | undefined {
{
const sip10Token = coins
.filter((coin) => coin instanceof Sip10Token && coin.assetId.includes(`${contractAddress}.${contractName}`))
.map((coin) => coin as Sip10Token);
return sip10Token ? sip10Token[0].assetId.split('::')[1] : undefined;
}
}
/**
* Function to get address and memo details from address input
*
* @param address
* @returns {AddressDetails}
*/
export function getMemoIdAndBaseAddressFromAddress(address: string): AddressDetails {
const [baseAddress, queryString] = address.split('?');
const params = new URLSearchParams(queryString);
const memoId = params.get('memoId');
return {
address: baseAddress,
memoId: memoId ? memoId : undefined,
};
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!