PHP WebShell
Текущая директория: /opt/BitGoJS/modules/abstract-substrate/src/lib
Просмотр файла: transaction.ts
import {
BaseKey,
BaseTransaction,
InvalidTransactionError,
ParseTransactionError,
SigningError,
TransactionRecipient,
TransactionType,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import Keyring, { decodeAddress } from '@polkadot/keyring';
import { u8aToBuffer } from '@polkadot/util';
import { construct, decode } from '@substrate/txwrapper-polkadot';
import { UnsignedTransaction } from '@substrate/txwrapper-core';
import { TypeRegistry } from '@substrate/txwrapper-core/lib/types';
import { KeyPair } from './keyPair';
import { DecodedTx, HexString, TransactionExplanation, TxData } from './iface';
import utils from './utils';
import { EXTRINSIC_VERSION } from '@polkadot/types/extrinsic/v4/Extrinsic';
import { DEFAULT_SUBSTRATE_PREFIX } from './constants';
export class Transaction extends BaseTransaction {
protected _substrateTransaction: UnsignedTransaction;
private _signedTransaction?: string;
private _registry: TypeRegistry;
private _chainName: string;
private _sender: string;
private static FAKE_SIGNATURE = `0x${Buffer.from(new Uint8Array(256).fill(1)).toString('hex')}`;
constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
}
/** @inheritdoc */
canSign({ key }: BaseKey): boolean {
const kp = new KeyPair({ prv: key });
const addr = kp.getAddress(this.getAddressFormat());
return addr === this._sender;
}
/**
* Sign a substrate transaction and update the transaction hex
*
* @param {KeyPair} keyPair - ed signature
*/
sign(keyPair: KeyPair): void {
if (!this._substrateTransaction) {
throw new InvalidTransactionError('No transaction data to sign');
}
const { prv, pub } = keyPair.getKeys();
if (!prv) {
throw new SigningError('Missing private key');
}
const signingPayload = construct.signingPayload(this._substrateTransaction, {
registry: this._registry,
});
// Sign a payload. This operation should be performed on an offline device.
const keyring = new Keyring({ type: 'ed25519' });
const secretKey = new Uint8Array(Buffer.from(prv, 'hex'));
const publicKey = new Uint8Array(Buffer.from(pub, 'hex'));
const signingKeyPair = keyring.addFromPair({ secretKey, publicKey });
const txHex = utils.createSignedTx(signingKeyPair, signingPayload, this._substrateTransaction, {
metadataRpc: this._substrateTransaction.metadataRpc,
registry: this._registry,
});
// get signature from signed txHex generated above
this._signatures = [utils.recoverSignatureFromRawTx(txHex, { registry: this._registry })];
this._signedTransaction = txHex;
}
/**
* Adds the signature to the Substrate Transaction
* @param {string} signature
*/
addSignature(signature: string): void {
this._signedTransaction = utils.serializeSignedTransaction(
this._substrateTransaction,
signature,
this._substrateTransaction.metadataRpc,
this._registry
);
}
/**
* Returns a serialized representation of this transaction with a fake signature attached which
* can be used to estimate transaction fees.
*/
fakeSign(): string {
return utils.serializeSignedTransaction(
this._substrateTransaction,
Transaction.FAKE_SIGNATURE,
this._substrateTransaction.metadataRpc,
this._registry
);
}
registry(registry: TypeRegistry): void {
this._registry = registry;
}
chainName(chainName: string): void {
this._chainName = chainName;
}
sender(sender: string): void {
this._sender = sender;
}
/** @inheritdoc */
toBroadcastFormat(): string {
if (!this._substrateTransaction) {
throw new InvalidTransactionError('Empty transaction');
}
if (this._signedTransaction && this._signedTransaction.length > 0) {
return this._signedTransaction;
} else {
return construct.signingPayload(this._substrateTransaction, {
registry: this._registry,
});
}
}
transactionSize(): number {
return this.toBroadcastFormat().length / 2;
}
/** @inheritdoc */
toJson(): TxData {
if (!this._substrateTransaction) {
throw new InvalidTransactionError('Empty transaction');
}
const decodedTx = decode(this._substrateTransaction, {
metadataRpc: this._substrateTransaction.metadataRpc,
registry: this._registry,
isImmortalEra: utils.isZeroHex(this._substrateTransaction.era),
}) as unknown as DecodedTx;
const result: TxData = {
id: construct.txHash(this.toBroadcastFormat()),
sender: decodedTx.address,
referenceBlock: decodedTx.blockHash,
blockNumber: decodedTx.blockNumber,
genesisHash: decodedTx.genesisHash,
nonce: decodedTx.nonce,
specVersion: decodedTx.specVersion,
transactionVersion: decodedTx.transactionVersion,
eraPeriod: decodedTx.eraPeriod,
chainName: this._chainName,
tip: decodedTx.tip ? Number(decodedTx.tip) : 0,
};
const txMethod = decodedTx.method.args;
if (this.type === TransactionType.Send) {
if (utils.isTransfer(txMethod)) {
const keypairDest = new KeyPair({
pub: Buffer.from(decodeAddress(txMethod.dest.id)).toString('hex'),
});
result.to = keypairDest.getAddress(this.getAddressFormat());
result.amount = txMethod.value;
} else if (utils.isTransferAll(txMethod)) {
const keypairDest = new KeyPair({
pub: Buffer.from(decodeAddress(txMethod.dest.id)).toString('hex'),
});
result.to = keypairDest.getAddress(this.getAddressFormat());
result.keepAlive = txMethod.keepAlive;
} else {
throw new ParseTransactionError(`Serializing unknown Transfer type parameters`);
}
} else if (this.type === TransactionType.StakingActivate) {
if (utils.isAddStake(txMethod)) {
const keypairDest = new KeyPair({
pub: Buffer.from(decodeAddress(txMethod.hotkey)).toString('hex'),
});
// hotkey address of validator
result.to = keypairDest.getAddress(this.getAddressFormat());
result.amount = txMethod.amountStaked.toString();
result.netuid = txMethod.netuid;
}
} else if (this.type === TransactionType.StakingDeactivate) {
if (utils.isRemoveStake(txMethod)) {
const keypairDest = new KeyPair({
pub: Buffer.from(decodeAddress(txMethod.hotkey)).toString('hex'),
});
// hotkey address of validator
result.to = keypairDest.getAddress(this.getAddressFormat());
result.amount = txMethod.amountUnstaked.toString();
result.netuid = txMethod.netuid;
}
}
return result;
}
explainTransferTransaction(json: TxData, explanationResult: TransactionExplanation): TransactionExplanation {
return {
...explanationResult,
outputs: [
{
address: json.to?.toString() || '',
amount: json.amount?.toString() || '',
},
],
};
}
explainStakeTransaction(json: TxData, explanationResult: TransactionExplanation): TransactionExplanation {
return {
...explanationResult,
outputs: [
{
address: json.to?.toString() || '',
amount: json.amount?.toString() || '',
},
],
};
}
explainUnstakeTransaction(json: TxData, explanationResult: TransactionExplanation): TransactionExplanation {
return {
...explanationResult,
outputs: [
{
address: json.sender.toString() || '',
amount: json.amount?.toString() || '',
},
],
};
}
/** @inheritdoc */
explainTransaction(): TransactionExplanation {
const result = this.toJson();
const outputs: TransactionRecipient[] = [];
const explanationResult: TransactionExplanation = {
// txhash used to identify the transactions
id: result.id,
outputAmount: result.amount?.toString() || '0',
changeAmount: '0',
changeOutputs: [],
outputs,
fee: {
fee: result.tip?.toString() || '',
type: 'tip',
},
type: this.type,
};
switch (this.type) {
case TransactionType.Send:
return this.explainTransferTransaction(result, explanationResult);
case TransactionType.StakingActivate:
return this.explainStakeTransaction(result, explanationResult);
case TransactionType.StakingDeactivate:
return this.explainUnstakeTransaction(result, explanationResult);
default:
throw new InvalidTransactionError('Transaction type not supported');
}
}
/**
* Load the input and output data on this transaction.
*/
loadInputsAndOutputs(): void {
if (!this._substrateTransaction) {
return;
}
const decodedTx = decode(this._substrateTransaction, {
metadataRpc: this._substrateTransaction.metadataRpc,
registry: this._registry,
isImmortalEra: utils.isZeroHex(this._substrateTransaction.era),
}) as unknown as DecodedTx;
if (this.type === TransactionType.Send) {
this.decodeInputsAndOutputsForSend(decodedTx);
} else if (this.type === TransactionType.StakingActivate) {
this.decodeInputsAndOutputsForStakingActivate(decodedTx);
} else if (this.type === TransactionType.StakingDeactivate) {
this.decodeInputsAndOutputsForStakingDeactivate(decodedTx);
}
}
private decodeInputsAndOutputsForSend(decodedTx: DecodedTx) {
const txMethod = decodedTx.method.args;
let to: string;
let value: string;
let from: string;
if (utils.isTransferAll(txMethod)) {
const keypairDest = new KeyPair({
pub: Buffer.from(decodeAddress(txMethod.dest.id)).toString('hex'),
});
to = keypairDest.getAddress(this.getAddressFormat());
value = '0'; // substrate transferAll's do not deserialize amounts
from = decodedTx.address;
} else if (utils.isTransfer(txMethod)) {
const keypairDest = new KeyPair({
pub: Buffer.from(decodeAddress(txMethod.dest.id)).toString('hex'),
});
to = keypairDest.getAddress(this.getAddressFormat());
value = txMethod.value;
from = decodedTx.address;
} else {
throw new ParseTransactionError(`Loading inputs of unknown Transfer type parameters`);
}
this._outputs = [
{
address: to,
value,
coin: this._coinConfig.name,
},
];
this._inputs = [
{
address: from,
value,
coin: this._coinConfig.name,
},
];
}
private decodeInputsAndOutputsForStakingActivate(decodedTx: DecodedTx) {
const txMethod = decodedTx.method.args;
let to: string;
let value: string;
let from: string;
if (utils.isAddStake(txMethod)) {
const keypairDest = new KeyPair({
pub: Buffer.from(decodeAddress(txMethod.hotkey)).toString('hex'),
});
to = keypairDest.getAddress(this.getAddressFormat());
value = txMethod.amountStaked.toString();
from = decodedTx.address;
} else {
throw new ParseTransactionError(`Loading inputs of unknown StakingActivate type parameters`);
}
this._outputs = [
{
address: to,
value,
coin: this._coinConfig.name,
},
];
this._inputs = [
{
address: from,
value,
coin: this._coinConfig.name,
},
];
}
private decodeInputsAndOutputsForStakingDeactivate(decodedTx: DecodedTx) {
const txMethod = decodedTx.method.args;
let to: string;
let value: string;
let from: string;
if (utils.isRemoveStake(txMethod)) {
const keypairDest = new KeyPair({
pub: Buffer.from(decodeAddress(txMethod.hotkey)).toString('hex'),
});
to = keypairDest.getAddress(this.getAddressFormat());
value = txMethod.amountUnstaked.toString();
from = decodedTx.address;
} else {
throw new ParseTransactionError(`Loading inputs of unknown StakingDeactivate type parameters`);
}
this._outputs = [
{
address: from,
value,
coin: this._coinConfig.name,
},
];
this._inputs = [
{
address: to,
value,
coin: this._coinConfig.name,
},
];
}
/**
* Constructs a signed payload using construct.signTx
* This method will be called during the build step if a TSS signature
* is added and will set the signTransaction which is the txHex that will be broadcasted
* As well as add the signature used to sign to the signature array in hex format
*
* @param {Buffer} signature The signature to be added to a substrate transaction
*/
constructSignedPayload(signature: Buffer): void {
// 0x00 means its an ED25519 signature
const edSignature = `0x00${signature.toString('hex')}` as HexString;
try {
this._signedTransaction = construct.signedTx(this._substrateTransaction, edSignature, {
registry: this._registry,
metadataRpc: this._substrateTransaction.metadataRpc,
});
} catch (e) {
throw new SigningError(`Unable to sign transaction with signature ${edSignature} ` + e);
}
this._signatures = [signature.toString('hex')];
}
setTransaction(tx: UnsignedTransaction): void {
this._substrateTransaction = tx;
}
/** @inheritdoc **/
get signablePayload(): Buffer {
const extrinsicPayload = this._registry.createType('ExtrinsicPayload', this._substrateTransaction, {
version: EXTRINSIC_VERSION,
});
return u8aToBuffer(extrinsicPayload.toU8a({ method: true }));
}
/**
* Set the transaction type.
*
* @param {TransactionType} transactionType The transaction type to be set.
*/
transactionType(transactionType: TransactionType): void {
this._type = transactionType;
}
protected getAddressFormat(): number {
return DEFAULT_SUBSTRATE_PREFIX;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!