PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-eos/src
Просмотр файла: eos.ts
/**
* @prettier
*/
import { BigNumber } from 'bignumber.js';
import { bip32, BIP32Interface } from '@bitgo/secp256k1';
import { createHash, randomBytes } from 'crypto';
import { Api, ApiInterfaces, JsonRpc, RpcInterfaces } from 'eosjs';
import * as ecc from 'eosjs-ecc';
import * as _ from 'lodash';
import * as querystring from 'querystring';
import * as request from 'superagent';
import * as url from 'url';
import { OfflineAbiProvider } from './eosutil/eosabiprovider';
import { StringTextDecoder } from './lib/utils';
import {
BaseCoin,
BitGoBase,
checkKrsProvider,
Environments,
getBip32Keys,
getIsKrsRecovery,
getIsUnsignedSweep,
HalfSignedAccountTransaction as BaseHalfSignedTransaction,
InvalidAddressError,
KeyPair,
MultisigType,
multisigTypes,
ParsedTransaction,
ParseTransactionOptions,
RequestTracer,
SignTransactionOptions as BaseSignTransactionOptions,
TransactionExplanation,
UnexpectedAddressError,
VerificationOptions,
VerifyAddressOptions as BaseVerifyAddressOptions,
VerifyTransactionOptions as BaseVerifyTransactionOptions,
Wallet,
} from '@bitgo/sdk-core';
interface AddressDetails {
address: string;
memoId?: string;
}
export interface EosTx {
signatures: string[];
packed_trx: string;
compression: string;
}
export interface Recipient {
address: string;
amount: string;
}
interface EosTransactionHeaders {
ref_block_prefix: number;
ref_block_num: number;
expiration?: string;
}
interface EosTransactionAction {
account: string;
name: string;
authorization: [{ actor: string; permission: string }];
data: TransferActionData | StakeActionData | VoteActionData;
}
interface EosTransactionPrebuild {
recipients: Recipient[];
headers: EosTransactionHeaders;
txHex: string; // The signable tx hex string
transaction: EosTx;
txid: string;
coin: string;
// full token name with the format, [t]eos:SYMBOL. This will only be present for token transactions. e.g. teos:CHEX.
token?: string;
}
export interface EosSignTransactionParams extends BaseSignTransactionOptions {
prv: string;
txPrebuild: EosTransactionPrebuild;
recipients: Recipient[];
}
export interface EosVerifyTransactionOptions extends BaseVerifyTransactionOptions {
txPrebuild: EosTransactionPrebuild;
txParams: EosSignTransactionParams;
wallet: Wallet;
verification?: VerificationOptions;
reqId?: RequestTracer;
}
export interface EosHalfSigned {
recipients: Recipient[];
headers: EosTransactionHeaders;
txHex: string; // The signable tx hex string
transaction: EosTx;
txid: string;
}
export interface EosSignedTransaction extends BaseHalfSignedTransaction {
halfSigned: EosHalfSigned;
}
interface DeserializedEosTransaction {
expiration: string;
ref_block_num: string;
ref_block_prefix: string;
max_net_usage_words: number;
max_cpu_usage_ms: number;
delay_sec: number;
context_free_actions: EosTransactionAction[];
actions: EosTransactionAction[];
transaction_extensions: Record<string, unknown>[];
address: string;
amount: string;
transaction_id: string;
memo?: string;
proxy?: string;
producers?: string[];
}
interface TransferActionData {
from: string;
to: string;
quantity: string;
memo?: string;
}
interface StakeActionData {
address: string;
amount: string;
from: string;
receiver: string;
transfer: number;
stake_cpu_quantity: string;
}
interface UnstakeActionData {
address: string;
amount: string;
from: string;
receiver: string;
unstake_cpu_quantity: string;
unstake_net_quantity: string;
}
interface RefundActionData {
address: string;
owner: string;
}
interface VoteActionData {
address: string;
proxy: string;
producers: string[];
voter: string;
}
interface ExplainTransactionOptions {
transaction: { packed_trx: string };
headers: EosTransactionHeaders;
}
interface RecoveryTransaction {
transaction: EosTx;
txid: string;
recoveryAmount: number;
}
interface RecoveryOptions {
userKey: string; // Box A
backupKey: string; // Box B
bitgoKey?: string; // Box C
recoveryDestination: string;
krsProvider?: string;
walletPassphrase?: string;
rootAddress?: string;
}
interface VerifyAddressOptions extends BaseVerifyAddressOptions {
rootAddress: string;
}
class NoopJsonRpc extends JsonRpc {
constructor() {
super('');
}
}
class NoopSignatureProvider implements ApiInterfaces.SignatureProvider {
async getAvailableKeys(): Promise<string[]> {
throw new Error('noop signature provider implementation has no available keys');
}
async sign(args: ApiInterfaces.SignatureProviderArgs): Promise<RpcInterfaces.PushTransactionArgs> {
throw new Error('noop implementation is unable to sign');
}
}
export class Eos extends BaseCoin {
public static VALID_ADDRESS_CHARS = '12345abcdefghijklmnopqrstuvwxyz'.split('');
public static ADDRESS_LENGTH = 12;
static createInstance(bitgo: BitGoBase): BaseCoin {
return new Eos(bitgo);
}
getChainId(): string {
return 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906'; // mainnet chain id
}
getChain(): string {
return 'eos';
}
getFamily(): string {
return 'eos';
}
getFullName(): string {
return 'EOS';
}
getBaseFactor(): number {
return 1e4;
}
get decimalPlaces() {
return 4;
}
/** {@inheritDoc } **/
supportsMultisig() {
return true;
}
/** inherited doc */
getDefaultMultisigType(): MultisigType {
return multisigTypes.onchain;
}
/**
* Flag for sending value of 0
* @returns {boolean} True if okay to send 0 value, false otherwise
*/
valuelessTransferAllowed(): boolean {
return true;
}
/**
* Get URLs of some active public nodes
*/
getPublicNodeUrls(): string[] {
return Environments[this.bitgo.getEnv()].eosNodeUrls;
}
/**
* Generate secp256k1 key pair
*
* @param seed - Seed from which the new keypair should be generated, otherwise a random seed is used
*/
generateKeyPair(seed?: Buffer): KeyPair {
if (!seed) {
// An extended private key has both a normal 256 bit private key and a 256
// bit chain code, both of which must be random. 512 bits is therefore the
// maximum entropy and gives us maximum security against cracking.
seed = randomBytes(512 / 8);
}
const extendedKey = bip32.fromSeed(seed);
const xpub = extendedKey.neutered().toBase58();
return {
pub: xpub,
prv: extendedKey.toBase58(),
};
}
/**
* Return boolean indicating whether input is valid public key for the coin.
*
* @param pub - the pub to be checked
*/
isValidPub(pub: string): boolean {
try {
return bip32.fromBase58(pub).isNeutered();
} catch (e) {
return false;
}
}
/**
* Return boolean indicating whether input is valid seed for the coin
*
* @param prv - the prv to be checked
*/
isValidPrv(prv: string): boolean {
try {
return !bip32.fromBase58(prv).isNeutered();
} catch (e) {
return false;
}
}
/**
* Evaluates whether a memo is valid
*
* @param value - the memo to be checked
*/
isValidMemo({ value }: { value: string }): boolean {
return _.isString(value) && value.length <= 256;
}
/**
* Return boolean indicating whether a memo id is valid
*
* @param memoId - the memo id to be checked
*/
isValidMemoId(memoId: string): boolean {
return this.isValidMemo({ value: memoId });
}
/**
* Process address into address and memo id
* @param address - the address
*/
getAddressDetails(address: string): AddressDetails {
const destinationDetails = url.parse(address);
const destinationAddress = destinationDetails.pathname;
if (!destinationAddress) {
throw new InvalidAddressError(`failed to parse address: ${address}`);
}
// EOS addresses have to be "human readable", which means up to 12 characters and only a-z1-5., i.e.mtoda1.bitgo
// source: https://developers.eos.io/eosio-cpp/docs/naming-conventions
if (!/^[a-z1-5.]*$/.test(destinationAddress) || destinationAddress.length > Eos.ADDRESS_LENGTH) {
throw new InvalidAddressError(`invalid address: ${address}`);
}
// address doesn't have a memo id
if (destinationDetails.pathname === address) {
return {
address: address,
memoId: undefined,
};
}
if (!destinationDetails.query) {
throw new InvalidAddressError(`failed to parse query string: ${address}`);
}
const queryDetails = querystring.parse(destinationDetails.query);
if (!queryDetails.memoId) {
// if there are more properties, the query details need to contain the memoId property
throw new InvalidAddressError(`invalid property in address: ${address}`);
}
if (Array.isArray(queryDetails.memoId) && queryDetails.memoId.length !== 1) {
// valid addresses can only contain one memo id
throw new InvalidAddressError(`invalid address '${address}', must contain exactly one memoId`);
}
const [memoId] = _.castArray(queryDetails.memoId);
if (!this.isValidMemoId(memoId)) {
throw new InvalidAddressError(`invalid address: '${address}', memoId is not valid`);
}
return {
address: destinationAddress,
memoId,
};
}
/**
* Convert a currency amount represented in base units (satoshi, wei, atoms, drops, stroops)
* to big units (btc, eth, xrp, xlm)
*/
baseUnitsToBigUnits(baseUnits: string | number): string {
const dividend = this.getBaseFactor();
const bigNumber = new BigNumber(baseUnits).dividedBy(dividend);
// set the format so commas aren't added to large coin amounts
return bigNumber.toFormat(this.decimalPlaces, null as any, { groupSeparator: '', decimalSeparator: '.' });
}
/**
* Validate and return address with appended memo id
*
* @param address
* @param memoId
*/
normalizeAddress({ address, memoId }: AddressDetails): string {
if (memoId && this.isValidMemoId(memoId)) {
return `${address}?memoId=${memoId}`;
}
return address;
}
/**
* Return boolean indicating whether input is valid public key for the coin
*
* @param address - the address to be checked
*/
isValidAddress(address: string): boolean {
try {
const addressDetails = this.getAddressDetails(address);
return address === this.normalizeAddress(addressDetails);
} catch (e) {
return false;
}
}
/**
* @param address - the address to verify
* @param rootAddress - the wallet's root address
* @return true iff address is a wallet address (based on rootAddress)
*/
async isWalletAddress({ address, rootAddress }: VerifyAddressOptions): Promise<boolean> {
if (!rootAddress || !_.isString(rootAddress)) {
throw new Error('missing required string rootAddress');
}
if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}
const addressDetails = this.getAddressDetails(address);
const rootAddressDetails = this.getAddressDetails(rootAddress);
if (!addressDetails || !rootAddressDetails) {
return false;
}
if (addressDetails.address !== rootAddressDetails.address) {
throw new UnexpectedAddressError(
`address validation failure: ${addressDetails.address} vs ${rootAddressDetails.address}`
);
}
return true;
}
/**
* Assemble keychain and half-sign prebuilt transaction
*
* @param params
* @param params.txPrebuild {Object} prebuild object returned by platform
* @param params.prv {String} user prv
* @returns {Promise<EosSignedTransaction>}
*/
async signTransaction(params: EosSignTransactionParams): Promise<EosSignedTransaction> {
const prv: string = params.prv;
const txHex: string = params.txPrebuild.txHex;
const transaction: EosTx = params.txPrebuild.transaction;
const signBuffer: Buffer = Buffer.from(txHex, 'hex');
const privateKeyBuffer = bip32.fromBase58(prv).privateKey;
if (!privateKeyBuffer) {
throw new Error('no privateKey');
}
const signature: string = ecc.Signature.sign(signBuffer, privateKeyBuffer).toString();
transaction.signatures.push(signature);
const txParams = {
transaction,
txHex,
recipients: params.txPrebuild.recipients,
headers: params.txPrebuild.headers,
txid: params.txPrebuild.txid,
};
return { halfSigned: txParams };
}
private validateStakeActionData(stakeActionData: StakeActionData): any {
if (stakeActionData.from !== stakeActionData.receiver) {
throw new Error(`staker (${stakeActionData.from}) and receiver (${stakeActionData.receiver}) must be the same`);
}
if (stakeActionData.transfer !== 0) {
throw new Error('cannot transfer funds as part of delegatebw action');
}
// stake_cpu_quantity is used as the amount because the BitGo platform only stakes cpu for voting transactions
return {
address: stakeActionData.from,
amount: this.bigUnitsToBaseUnits(stakeActionData.stake_cpu_quantity.split(' ')[0]),
};
}
private validateUnstakeActionData(unstakeActionData: UnstakeActionData): any {
if (unstakeActionData.from !== unstakeActionData.receiver) {
throw new Error(
`unstaker (${unstakeActionData.from}) and receiver (${unstakeActionData.receiver}) must be the same`
);
}
const cpuAmount = new BigNumber(unstakeActionData.unstake_cpu_quantity.split(' ')[0]);
const netAmount = new BigNumber(unstakeActionData.unstake_net_quantity.split(' ')[0]);
const totalAmount = cpuAmount.plus(netAmount).toNumber();
return {
address: unstakeActionData.receiver,
amount: this.bigUnitsToBaseUnits(totalAmount),
};
}
private static validateVoteActionData(voteActionData: VoteActionData) {
const proxyIsEmpty = _.isEmpty(voteActionData.proxy);
const producersIsEmpty = _.isEmpty(voteActionData.producers);
if ((proxyIsEmpty && producersIsEmpty) || (!proxyIsEmpty && !producersIsEmpty)) {
throw new Error('voting transactions must specify either producers or proxy to vote for');
}
return {
address: voteActionData.voter,
proxy: voteActionData.proxy,
producers: voteActionData.producers,
};
}
private static createTransactionIdHex(serializedTransactionBuffer: Buffer): string {
return createHash('sha256').update(serializedTransactionBuffer).digest().toString('hex');
}
/**
* Deserialize a transaction
* @param transaction
* @param headers
*/
private async deserializeTransaction({
transaction,
headers,
}: ExplainTransactionOptions): Promise<DeserializedEosTransaction> {
// create an eosjs API client
const api = new Api({
abiProvider: new OfflineAbiProvider(),
rpc: new NoopJsonRpc(),
signatureProvider: new NoopSignatureProvider(),
chainId: this.getChainId(),
// Use a custom TextDecoder as the global TextDecoder leads to crashes in OVC / Electron.
textDecoder: new StringTextDecoder(),
textEncoder: new TextEncoder(),
});
// type guards
const isTransferActionData = (txActionData: any): txActionData is TransferActionData => {
return (
(txActionData as TransferActionData).from !== undefined &&
(txActionData as TransferActionData).to !== undefined &&
(txActionData as TransferActionData).quantity !== undefined
);
};
const isStakeActionData = (txActionData: any): txActionData is StakeActionData => {
return (
(txActionData as StakeActionData).from !== undefined &&
(txActionData as StakeActionData).receiver !== undefined &&
(txActionData as StakeActionData).transfer !== undefined &&
(txActionData as StakeActionData).stake_cpu_quantity !== undefined
);
};
const isUnstakeActionData = (txActionData: any): txActionData is UnstakeActionData => {
return (
(txActionData as UnstakeActionData).from !== undefined &&
(txActionData as UnstakeActionData).receiver !== undefined &&
(txActionData as UnstakeActionData).unstake_cpu_quantity !== undefined &&
(txActionData as UnstakeActionData).unstake_net_quantity !== undefined
);
};
const isVoteActionData = (txActionData: any): txActionData is VoteActionData => {
return (txActionData as VoteActionData).voter !== undefined;
};
const isRefundActionData = (txActionData: any): txActionData is RefundActionData => {
return (txActionData as RefundActionData).owner !== undefined;
};
// deserializeTransaction
const serializedTxBuffer = Buffer.from(transaction.packed_trx, 'hex');
const deserializedTxJsonFromPackedTrx = await api.deserializeTransactionWithActions(serializedTxBuffer);
if (!deserializedTxJsonFromPackedTrx) {
throw new Error('could not process transaction from txHex');
}
const tx: DeserializedEosTransaction = deserializedTxJsonFromPackedTrx;
// validate context free actions
if (tx.context_free_actions.length !== 0) {
if (tx.context_free_actions.length !== 1) {
throw new Error('number of context free actions must be 1');
}
if (
!_.isEqual(_.pick(tx.context_free_actions[0], ['account', 'authorization', 'name']), {
account: 'eosio.null',
authorization: [],
name: 'nonce',
}) ||
_.isEmpty(tx.context_free_actions[0].data)
) {
throw new Error('the context free action is invalid');
}
}
// Only support transactions with one (transfer | voteproducer) or two (delegatebw & voteproducer) actions
if (tx.actions.length !== 1 && tx.actions.length !== 2) {
throw new Error(`invalid number of actions: ${tx.actions.length}`);
}
const txAction = tx.actions[0];
if (!txAction) {
throw new Error('missing transaction action');
}
if (txAction.name === 'transfer') {
// Transfers should only have 1 action
if (tx.actions.length !== 1) {
throw new Error(`transfers should only have 1 action: ${tx.actions.length} given`);
}
if (!isTransferActionData(txAction.data)) {
throw new Error('Invalid or incomplete transfer action data');
}
const transferActionData = txAction.data;
tx.address = transferActionData.to;
tx.amount = this.bigUnitsToBaseUnits(transferActionData.quantity.split(' ')[0]);
tx.memo = transferActionData.memo;
} else if (txAction.name === 'delegatebw') {
// The delegatebw action should only be part of voting transactions
if (tx.actions.length !== 2) {
throw new Error(
`staking transactions that include the delegatebw action should have 2 actions: ${tx.actions.length} given`
);
}
const txAction2 = tx.actions[1];
if (txAction2.name !== 'voteproducer') {
throw new Error(`invalid staking transaction action: ${txAction2.name}, expecting: voteproducer`);
}
if (!isStakeActionData(txAction.data) || !isVoteActionData(txAction2.data)) {
throw new Error('Invalid or incomplete stake or vote action data');
}
const stakeActionData = txAction.data;
const voteActionData = txAction2.data;
const deserializedStakeAction = this.validateStakeActionData(stakeActionData);
const deserializedVoteAction = Eos.validateVoteActionData(voteActionData);
if (deserializedStakeAction.address !== deserializedVoteAction.address) {
throw new Error(
`staker (${deserializedStakeAction.address}) and voter (${deserializedVoteAction.address}) must be the same`
);
}
tx.amount = deserializedStakeAction.amount;
tx.proxy = deserializedVoteAction.proxy;
tx.producers = deserializedVoteAction.producers;
} else if (txAction.name === 'voteproducer') {
if (tx.actions.length > 2) {
throw new Error('voting transactions should not have more than 2 actions');
}
let deserializedStakeAction;
if (tx.actions.length === 2) {
const txAction2 = tx.actions[1];
if (txAction2.name !== 'delegatebw') {
throw new Error(`invalid staking transaction action: ${txAction2.name}, expecting: delegatebw`);
}
if (!isStakeActionData(txAction.data)) {
throw new Error('Invalid or incomplete stake action data');
}
const stakeActionData = txAction.data;
deserializedStakeAction = this.validateStakeActionData(stakeActionData);
}
if (!isVoteActionData(txAction.data)) {
throw new Error('Invalid or incomplete vote action data');
}
const voteActionData = txAction.data;
const deserializedVoteAction = Eos.validateVoteActionData(voteActionData);
if (!!deserializedStakeAction && deserializedStakeAction.address !== deserializedVoteAction.address) {
throw new Error(
`staker (${deserializedStakeAction.address}) and voter (${deserializedVoteAction.address}) must be the same`
);
}
tx.amount = !!deserializedStakeAction ? deserializedStakeAction.amount : '0';
tx.proxy = deserializedVoteAction.proxy;
tx.producers = deserializedVoteAction.producers;
} else if (txAction.name === 'undelegatebw') {
if (tx.actions.length !== 1) {
throw new Error(`unstake should only have 1 action: ${tx.actions.length} given`);
}
if (!isUnstakeActionData(txAction.data)) {
throw new Error('Invalid or incomplete unstake action data');
}
const unstakeActionData = txAction.data;
const deserializedUnstakeAction = this.validateUnstakeActionData(unstakeActionData);
tx.amount = deserializedUnstakeAction.amount;
tx.address = deserializedUnstakeAction.address;
} else if (txAction.name === 'refund') {
if (tx.actions.length !== 1) {
throw new Error(`refund should only have 1 action: ${tx.actions.length} given`);
}
if (!isRefundActionData(txAction.data)) {
throw new Error('Invalid or incomplete refund action data');
}
const refundActionData = txAction.data;
tx.address = refundActionData.owner;
tx.amount = '0';
} else {
throw new Error(`invalid action: ${txAction.name}`);
}
// Get the tx id if tx headers were provided
if (headers) {
let rebuiltTransaction;
try {
// remove Z at the end
if ((headers.expiration as string).endsWith('Z')) {
headers.expiration = (headers.expiration as string).slice(0, -1);
}
rebuiltTransaction = await api.transact({ ...tx, ...headers }, { sign: false, broadcast: false });
} catch (e) {
throw new Error(
'Could not build transaction to get transaction_id. Please check transaction or headers format.'
);
}
tx.transaction_id = Eos.createTransactionIdHex((rebuiltTransaction as any).serializedTransaction);
}
return tx;
}
/**
* Explain/parse transaction
* @param params - ExplainTransactionOptions
*/
async explainTransaction(params: ExplainTransactionOptions): Promise<TransactionExplanation> {
let transaction;
try {
transaction = await this.deserializeTransaction(params);
} catch (e) {
throw new Error('invalid EOS transaction or headers: ' + e.toString());
}
return {
displayOrder: [
'id',
'outputAmount',
'changeAmount',
'outputs',
'changeOutputs',
'fee',
'memo',
'proxy',
'producers',
],
id: transaction.transaction_id,
changeOutputs: [],
outputAmount: transaction.amount,
changeAmount: 0,
outputs: !!transaction.address ? [{ address: transaction.address, amount: transaction.amount }] : [],
fee: {},
memo: transaction.memo,
proxy: transaction.proxy,
producers: transaction.producers,
} as any;
}
/**
* @deprecated
*/
initiateRecovery(params: RecoveryOptions): never {
throw new Error('deprecated method');
}
/**
* Make a request to one of the public EOS nodes available
* @param params.endpoint
* @param params.payload
*/
protected async getDataFromNode(params: {
endpoint: string;
payload?: Record<string, unknown>;
}): Promise<request.Response> {
const nodeUrls = this.getPublicNodeUrls();
for (const nodeUrl of nodeUrls) {
try {
return await request.post(nodeUrl + params.endpoint).send(params.payload);
} catch (e) {
// let's hope another call succeeds
}
}
throw new Error(`Unable to call endpoint: ${params.endpoint} from nodes: ${_.join(nodeUrls, ', ')}`);
}
/**
* Get EOS chain info from a public node
*/
protected async getChainInfoFromNode(): Promise<any> {
const response = await this.getDataFromNode({ endpoint: '/v1/chain/get_info' });
if (response.status !== 200) {
throw new Error('Unable to fetch chain info');
}
return response.body;
}
/**
* Get data specific to an account from a public node
* @param address
*/
protected async getAccountFromNode({ address }: { address: string }): Promise<any> {
const response = await this.getDataFromNode({
endpoint: '/v1/chain/get_account',
payload: { account_name: address },
});
if (response.status !== 200) {
throw new Error('Account not found');
}
return response.body;
}
/**
* Get block data from a public node using its block number or block id
* @param blockNumOrId
*/
protected async getBlockFromNode({ blockNumOrId }: { blockNumOrId: string }): Promise<any> {
const response = await this.getDataFromNode({
endpoint: '/v1/chain/get_block',
payload: { block_num_or_id: blockNumOrId },
});
if (response.status !== 200) {
throw new Error('Block not found');
}
return response.body;
}
/**
* Get headers for an EOS tx from a public node
*/
protected async getTransactionHeadersFromNode(): Promise<any> {
const chainInfo = await this.getChainInfoFromNode();
const headBlockInfoResult = await this.getBlockFromNode({ blockNumOrId: chainInfo.head_block_num });
const expireSeconds = 28800; // maximum tx expire time of 8h
const chainDate = new Date(chainInfo.head_block_time + 'Z').getTime();
const expirationDate = new Date(chainDate + expireSeconds * 1000);
return {
expiration: expirationDate.toISOString(),
ref_block_num: chainInfo.head_block_num & 0xffff,
ref_block_prefix: headBlockInfoResult.ref_block_prefix,
};
}
protected getTransferAction({ recipient, sender, amount, memo }: any): EosTransactionAction {
return {
account: 'eosio.token',
name: 'transfer',
authorization: [
{
actor: sender,
permission: 'active',
},
],
data: {
from: sender,
to: recipient,
quantity: `${this.baseUnitsToBigUnits(amount)} EOS`,
memo: !_.isNil(memo) ? memo : '', // Memo must be defined, set it to empty string if it is not
},
};
}
/**
* Sign a transaction with a key
* @param signableTx
* @param signingKey
*/
signTx(signableTx: string, signingKey: BIP32Interface): string {
const signBuffer = Buffer.from(signableTx, 'hex');
const privateKeyBuffer = signingKey.privateKey;
return ecc.Signature.sign(signBuffer, privateKeyBuffer).toString();
}
/**
* Builds a funds recovery transaction without BitGo
* @param params
*/
async recover(params: RecoveryOptions): Promise<RecoveryTransaction> {
if (!params.rootAddress) {
throw new Error('missing required string rootAddress');
}
const isKrsRecovery = getIsKrsRecovery(params);
const isUnsignedSweep = getIsUnsignedSweep(params);
const { krsProvider } = params;
if (getIsKrsRecovery(params)) {
checkKrsProvider(this, krsProvider);
}
if (!this.isValidAddress(params.recoveryDestination)) {
throw new Error('Invalid destination address!');
}
const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false });
const rootAddressDetails = this.getAddressDetails(params.rootAddress);
const account = await this.getAccountFromNode({ address: rootAddressDetails.address });
if (!account.core_liquid_balance) {
throw new Error('Could not find any balance to recovery for ' + params.rootAddress);
}
if (!account.permissions) {
throw new Error('Could not find permissions for ' + params.rootAddress);
}
const userPub = ecc.PublicKey.fromBuffer(keys[0].publicKey).toString();
const backupPub = ecc.PublicKey.fromBuffer(keys[1].publicKey).toString();
const activePermission = _.find(account.permissions, { perm_name: 'active' });
const requiredAuth = _.get(activePermission, 'required_auth');
if (!requiredAuth) {
throw new Error('Required auth for active permission not found in account');
}
if (requiredAuth.threshold !== 2) {
throw new Error('Unexpected active permission threshold');
}
const foundPubs = {};
const requiredAuthKeys = requiredAuth.keys;
for (const signer of requiredAuthKeys) {
if (signer.weight !== 1) {
throw new Error('invalid signer weight');
}
// if it's a dupe of a pub we already know, block
if (foundPubs[signer.key]) {
throw new Error('duplicate signer key');
}
foundPubs[signer.key] = (foundPubs[signer.key] || 0) + 1;
}
if (foundPubs[userPub] !== 1 || foundPubs[backupPub] !== 1) {
throw new Error('unexpected incidence frequency of user signer key');
}
const accountBalance = account.core_liquid_balance.split(' ')[0];
const recoveryAmount = this.bigUnitsToBaseUnits(new BigNumber(accountBalance).toFixed());
const destinationAddress = params.recoveryDestination;
const destinationAddressDetails = this.getAddressDetails(destinationAddress);
const destinationAccount = await this.getAccountFromNode({ address: destinationAddressDetails.address });
if (!destinationAccount) {
throw new Error('Destination account not found');
}
const transactionHeaders = await this.getTransactionHeadersFromNode();
if (!transactionHeaders) {
throw new Error('Could not get transaction headers from node');
}
const headers: EosTransactionHeaders = transactionHeaders;
const nativeDate = new Date(headers.expiration as string);
// drop milliseconds and trailing Z from expiration
nativeDate.setMilliseconds(0);
const expiration = nativeDate.toISOString();
if (expiration.endsWith('Z')) {
headers.expiration = expiration.slice(0, -1);
}
// create an offline eosjs API client
const api = new Api({
rpc: new NoopJsonRpc(),
signatureProvider: new NoopSignatureProvider(),
abiProvider: new OfflineAbiProvider(),
chainId: this.getChainId(),
textDecoder: new TextDecoder(),
textEncoder: new TextEncoder(),
});
const transferAction = this.getTransferAction({
recipient: destinationAddressDetails.address,
sender: rootAddressDetails.address,
amount: new BigNumber(recoveryAmount),
memo: destinationAddressDetails.memoId,
});
let serializedTransaction;
const tx = { actions: [transferAction] };
try {
serializedTransaction = await api.transact({ ...tx, ...headers }, { sign: false, broadcast: false });
} catch (e) {
throw new Error('Eos API error: Could not build transaction');
}
// create transaction object
const serializedTransactionHex = Buffer.from(serializedTransaction.serializedTransaction).toString('hex');
const transactionId = Eos.createTransactionIdHex(serializedTransaction.serializedTransaction);
const txObject = {
transaction: {
compression: 'none',
packed_trx: serializedTransactionHex,
signatures: [] as string[],
},
txid: transactionId,
recoveryAmount: accountBalance,
coin: this.getChain(),
txHex: '',
};
const signableTx = Buffer.concat([
Buffer.from(this.getChainId(), 'hex'), // The ChainID representing the chain that we are on
Buffer.from(serializedTransaction.serializedTransaction), // The serialized unsigned tx
Buffer.from(new Uint8Array(32)), // Some padding
]).toString('hex');
if (isUnsignedSweep) {
txObject.txHex = signableTx;
return txObject;
}
const userSignature = this.signTx(signableTx, keys[0]);
txObject.transaction.signatures.push(userSignature);
if (!isKrsRecovery) {
const backupSignature = this.signTx(signableTx, keys[1]);
txObject.transaction.signatures.push(backupSignature);
}
return txObject;
}
async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
return {};
}
/**
* Verify that a transaction prebuild complies with the original intention
*
* @param params
* @param params.txParams params used to build the transaction
* @param params.txPrebuild the prebuilt transaction
*/
async verifyTransaction(params: EosVerifyTransactionOptions): Promise<any> {
const { txParams: txParams, txPrebuild: txPrebuild } = params;
// check if the transaction has a txHex
if (!txPrebuild.txHex) {
throw new Error('missing required tx prebuild property txHex');
}
// construct transaction from txHex
const txFromHex = Buffer.from(txPrebuild.txHex, 'hex');
const txDataWithPadding = txFromHex.slice(32);
const txData = txDataWithPadding.slice(0, txDataWithPadding.length - 32);
const deserializedTxJson = await this.deserializeTransaction({
transaction: { packed_trx: txData.toString('hex') },
headers: txPrebuild.headers,
});
if (!deserializedTxJson) {
throw new Error('could not process transaction from txHex');
}
const txJsonFromHex: DeserializedEosTransaction = deserializedTxJson;
// check that if txParams has a txPrebuild, it should be the same as txPrebuild
if (txParams.txPrebuild && !_.isEqual(txParams.txPrebuild, txPrebuild)) {
throw new Error('inputs txParams.txPrebuild and txPrebuild expected to be equal but were not');
}
// check if prebuild has a transaction
if (!txPrebuild.transaction) {
throw new Error('missing required transaction in txPrebuild');
}
// check if transaction has a packed_trx
if (!txPrebuild.transaction?.packed_trx) {
throw new Error('missing required transaction.packed_trx in txPrebuild');
}
// construct transaction using packed_trx
const deserializedTxJsonFromPackedTrx = await this.deserializeTransaction({
transaction: { packed_trx: txPrebuild.transaction.packed_trx },
headers: txPrebuild.headers,
});
if (!deserializedTxJsonFromPackedTrx) {
throw new Error('could not process transaction from packed_trx');
}
const txJsonFromPackedTrx: DeserializedEosTransaction = deserializedTxJsonFromPackedTrx;
// deep check of object from packed_trx and txHex
if (!_.isEqual(txJsonFromPackedTrx, txJsonFromHex)) {
throw new Error('unpacked packed_trx and unpacked txHex are not equal');
}
if (txParams.recipients.length > 1) {
throw new Error('only 0 or 1 recipients are supported');
}
// check the amounts, recipient, and coin name for transfers
if (txParams.recipients.length === 1) {
const expectedOutput = txParams.recipients[0];
// check output address and memoId
const expectedOutputAddressAndMemoId = this.getAddressDetails(expectedOutput.address);
const txHexAction = txJsonFromHex.actions[0];
const txHexTransferAction = txHexAction.data as TransferActionData;
if (txHexTransferAction.to !== expectedOutputAddressAndMemoId.address) {
throw new Error('txHex receive address does not match expected recipient address');
}
// check if txaction memoid is equal to address memo id only if address also has memoid present
if (!_.isUndefined(expectedOutputAddressAndMemoId.memoId)) {
if (txHexTransferAction.memo !== expectedOutputAddressAndMemoId.memoId) {
throw new Error('txHex receive memoId does not match expected recipient memoId');
}
}
// check amount and coin
const expectedOutputAmount = expectedOutput.amount;
const actualAmountAndCoin = txHexTransferAction.quantity.split(' ');
const actualOutputAmount = this.bigUnitsToBaseUnits(actualAmountAndCoin[0]);
if (expectedOutputAmount !== actualOutputAmount) {
throw new Error('txHex receive amount does not match expected recipient amount');
}
if (txPrebuild.coin === 'eos' || txPrebuild.coin === 'teos') {
const expectedSymbol = _.isNil(txPrebuild.token) ? 'EOS' : txPrebuild.token.split(':')[1];
if (actualAmountAndCoin[1] !== expectedSymbol) {
throw new Error('txHex receive symbol does not match expected recipient symbol');
}
} else {
// this should never happen
throw new Error('txHex coin name does not match expected coin name');
}
}
return true;
}
/**
* Generate a random EOS address.
*
* This is just a random string which abides by the EOS adddress constraints,
* and is not actually checked for availability on the EOS blockchain.
*
* Current EOS address constraints are:
* * Address must be exactly 12 characters
* * Address must only contain lowercase letters and numbers 1-5
* @returns a validly formatted EOS address, which may or may not actually be available on chain.
*/
generateRandomAddress(params: Record<string, never>): string {
const address: string[] = [];
while (address.length < 12) {
const char = _.sample(Eos.VALID_ADDRESS_CHARS);
if (!char) {
throw new Error('failed to sample valid EOS address characters');
}
address.push(char);
}
return address.join('');
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!