PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-rune/src
Просмотр файла: rune.ts
import {
CosmosCoin,
CosmosKeyPair,
CosmosLikeCoinRecoveryOutput,
CosmosTransaction,
FeeData,
GasAmountDetails,
RecoveryOptions,
SendMessage,
} from '@bitgo/abstract-cosmos';
import {
BaseCoin,
BitGoBase,
Ecdsa,
ECDSAUtils,
Environments,
TransactionType,
VerifyTransactionOptions,
} from '@bitgo/sdk-core';
import { BaseCoin as StaticsBaseCoin, BaseUnit, coins } from '@bitgo/statics';
import { KeyPair, TransactionBuilderFactory } from './lib';
import { GAS_AMOUNT, GAS_LIMIT, RUNE_FEES, ROOT_PATH } from './lib/constants';
import { RuneUtils } from './lib/utils';
import { BigNumber } from 'bignumber.js';
const bech32 = require('bech32-buffer');
import * as _ from 'lodash';
import { Coin } from '@cosmjs/stargate';
import { createHash } from 'crypto';
export class Rune extends CosmosCoin {
protected readonly _utils: RuneUtils;
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
super(bitgo, staticsCoin);
if (!staticsCoin) {
throw new Error('missing required constructor parameter staticsCoin');
}
this._staticsCoin = staticsCoin;
this._utils = new RuneUtils();
}
static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>): BaseCoin {
return new Rune(bitgo, staticsCoin);
}
/** @inheritDoc **/
getBuilder(): TransactionBuilderFactory {
return new TransactionBuilderFactory(coins.get(this.getChain()));
}
/**
* Factor between the coin's base unit and its smallest subdivison
*/
public getBaseFactor(): number {
return 1e8;
}
isValidAddress(address: string): boolean {
return this._utils.isValidAddress(address) || this._utils.isValidValidatorAddress(address);
}
/** @inheritDoc **/
protected getPublicNodeUrl(): string {
return Environments[this.bitgo.getEnv()].runeNodeUrl;
}
/** @inheritDoc **/
getDenomination(): string {
return BaseUnit.RUNE;
}
/** @inheritDoc **/
getGasAmountDetails(): GasAmountDetails {
return {
gasAmount: GAS_AMOUNT,
gasLimit: GAS_LIMIT,
};
}
/** @inheritDoc **/
getKeyPair(publicKey: string): CosmosKeyPair {
return new KeyPair({ pub: publicKey });
}
/** @inheritDoc **/
getAddressFromPublicKey(publicKey: string): string {
return new KeyPair({ pub: publicKey }).getAddress();
}
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
let totalAmount = new BigNumber(0);
const { txPrebuild, txParams } = params;
const rawTx = txPrebuild.txHex;
if (!rawTx) {
throw new Error('missing required tx prebuild property txHex');
}
const transaction = await this.getBuilder().from(rawTx).build();
const explainedTx = transaction.explainTransaction();
if (txParams.recipients && txParams.recipients.length > 0) {
const filteredRecipients = txParams.recipients?.map((recipient) => _.pick(recipient, ['address', 'amount']));
let filteredOutputs = explainedTx.outputs.map((output) => _.pick(output, ['address', 'amount']));
filteredOutputs = filteredOutputs.map((output) => {
const prefix = this._utils.getNetworkPrefix();
const convertedAddress = bech32.encode(prefix, output.address);
return {
...output,
address: convertedAddress,
};
});
if (!_.isEqual(filteredOutputs, filteredRecipients)) {
throw new Error('Tx outputs does not match with expected txParams recipients');
}
// WithdrawDelegatorRewards and ContractCall transaction don't have amount
if (transaction.type !== TransactionType.StakingWithdraw && transaction.type !== TransactionType.ContractCall) {
for (const recipients of txParams.recipients) {
totalAmount = totalAmount.plus(recipients.amount);
}
if (!totalAmount.isEqualTo(explainedTx.outputAmount)) {
throw new Error('Tx total amount does not match with expected total amount field');
}
}
}
return true;
}
getNativeRuneTxnFees(): string {
return RUNE_FEES;
}
/**
* This function is overridden from CosmosCoin class' recover function due to the difference in fees handling in thorchain
* @param {RecoveryOptions} params parameters needed to construct and
* (maybe) sign the transaction
*
* @returns {CosmosLikeCoinRecoveryOutput} the serialized transaction hex string and index
* of the address being swept
*/
async recover(params: RecoveryOptions): Promise<CosmosLikeCoinRecoveryOutput> {
// Step 1: Check if params contains the required parameters
if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) {
throw new Error('invalid recoveryDestination');
}
if (!params.userKey) {
throw new Error('missing userKey');
}
if (!params.backupKey) {
throw new Error('missing backupKey');
}
if (!params.walletPassphrase) {
throw new Error('missing wallet passphrase');
}
// Step 2: Fetch the bitgo key from params
const userKey = params.userKey.replace(/\s/g, '');
const backupKey = params.backupKey.replace(/\s/g, '');
const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils.getMpcV2RecoveryKeyShares(
userKey,
backupKey,
params.walletPassphrase
); // baseAddress is not extracted
// Step 3: Instantiate the ECDSA signer and fetch the address details
const MPC = new Ecdsa();
const chainId = await this.getChainId();
const publicKey = MPC.deriveUnhardened(commonKeyChain, ROOT_PATH).slice(0, 66);
const senderAddress = this.getAddressFromPublicKey(publicKey);
// Step 4: Fetch account details such as accountNo, balance and check for sufficient funds once gasAmount has been deducted
const [accountNumber, sequenceNo] = await this.getAccountDetails(senderAddress);
const balance = new BigNumber(await this.getAccountBalance(senderAddress));
const gasBudget: FeeData = {
amount: [{ denom: this.getDenomination(), amount: this.getGasAmountDetails().gasAmount }],
gasLimit: this.getGasAmountDetails().gasLimit,
};
const actualBalance = balance.minus(this.getNativeRuneTxnFees());
if (actualBalance.isLessThanOrEqualTo(0)) {
throw new Error('Did not have enough funds to recover');
}
// Step 5: Once sufficient funds are present, construct the recover tx messsage
const amount: Coin[] = [
{
denom: this.getDenomination(),
amount: actualBalance.toFixed(),
},
];
const sendMessage: SendMessage[] = [
{
fromAddress: senderAddress,
toAddress: params.recoveryDestination,
amount: amount,
},
];
// Step 6: Build the unsigned tx using the constructed message
const txnBuilder = this.getBuilder().getTransferBuilder();
txnBuilder
.messages(sendMessage)
.gasBudget(gasBudget)
.publicKey(publicKey)
.sequence(Number(sequenceNo))
.accountNumber(Number(accountNumber))
.chainId(chainId);
const unsignedTransaction = (await txnBuilder.build()) as CosmosTransaction;
let serializedTx = unsignedTransaction.toBroadcastFormat();
const signableHex = unsignedTransaction.signablePayload.toString('hex');
// Step 7: Sign the tx
const message = unsignedTransaction.signablePayload;
const messageHash = createHash('sha256').update(message).digest();
const signature = await ECDSAUtils.signRecoveryMpcV2(messageHash, userKeyShare, backupKeyShare, commonKeyChain);
const signableBuffer = Buffer.from(signableHex, 'hex');
MPC.verify(signableBuffer, signature, this.getHashFunction());
const cosmosKeyPair = this.getKeyPair(publicKey);
txnBuilder.addSignature({ pub: cosmosKeyPair.getKeys().pub }, Buffer.from(signature.r + signature.s, 'hex'));
const signedTransaction = await txnBuilder.build();
serializedTx = signedTransaction.toBroadcastFormat();
return { serializedTx: serializedTx };
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!