PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-xlm/src
Просмотр файла: xlm.ts
import assert from 'assert';
import * as _ from 'lodash';
import * as querystring from 'querystring';
import * as url from 'url';
import * as request from 'superagent';
import * as stellar from 'stellar-sdk';
import { BigNumber } from 'bignumber.js';
import * as Utils from './lib/utils';
import { KeyPair as StellarKeyPair } from './lib/keyPair';
import {
BaseCoin,
BitGoBase,
checkKrsProvider,
common,
ExtraPrebuildParamsOptions,
InvalidAddressError,
InvalidMemoIdError,
ITransactionRecipient,
KeyIndices,
KeyPair,
ParsedTransaction,
ParseTransactionOptions,
promiseProps,
SignTransactionOptions as BaseSignTransactionOptions,
StellarFederationUserNotFoundError,
TokenEnablementConfig,
TransactionExplanation as BaseTransactionExplanation,
TransactionParams as BaseTransactionParams,
TransactionPrebuild as BaseTransactionPrebuild,
TransactionRecipient as BaseTransactionOutput,
UnexpectedAddressError,
VerifyAddressOptions as BaseVerifyAddressOptions,
VerifyTransactionOptions as BaseVerifyTransactionOptions,
Wallet,
NotSupported,
MultisigType,
multisigTypes,
} from '@bitgo/sdk-core';
import { toBitgoRequest } from '@bitgo/sdk-api';
import { getStellarKeys } from './getStellarKeys';
/**
* XLM accounts support virtual (muxed) addresses
* A base address starts with "G" and is tied to the underlying "real" account
* A muxed address starts with "M" and combines the base address with a 64-bit integer ID in order to provide
* an alternative to memo ids.
*/
interface AddressDetails {
baseAddress: string;
address: string;
id?: string;
memoId?: string | undefined;
}
interface Memo {
type: stellar.MemoType;
value: string;
}
interface InitiateRecoveryOptions {
userKey: string;
backupKey: string;
recoveryDestination: string;
krsProvider?: string;
walletPassphrase?: string;
}
interface RecoveryOptions extends InitiateRecoveryOptions {
rootAddress?: string;
}
interface RecoveryTransaction {
txBase64: string;
recoveryAmount: number;
coin?: string;
backupKey?: string;
txInfo?: any;
feeInfo?: any;
}
interface BuildOptions {
wallet?: Wallet;
recipients?: Record<string, string>[];
type?: string;
walletPassphrase?: string;
[index: string]: unknown;
}
interface TransactionPrebuild extends BaseTransactionPrebuild {
txBase64: string;
}
interface SignTransactionOptions extends BaseSignTransactionOptions {
txPrebuild: TransactionPrebuild;
prv: string;
}
interface HalfSignedTransaction {
halfSigned: {
txBase64: string;
};
recipients?: ITransactionRecipient[];
type?: string;
}
interface SupplementGenerateWalletOptions {
rootPrivateKey?: string;
}
interface ExplainTransactionOptions {
txHex?: string;
txBase64?: string;
}
interface TransactionMemo {
value?: string;
type?: string;
}
interface TransactionOperation {
type: string;
coin: string;
limit?: string;
asset?: stellar.Asset;
}
interface TransactionOutput extends BaseTransactionOutput {
coin: string;
}
interface TransactionExplanation extends BaseTransactionExplanation {
memo: TransactionMemo;
}
interface VerifyAddressOptions extends BaseVerifyAddressOptions {
rootAddress: string;
}
interface TrustlineOptions {
token: string;
action: string;
limit?: string;
}
interface TransactionParams extends BaseTransactionParams {
trustlines?: TrustlineOptions[];
}
interface VerifyTransactionOptions extends BaseVerifyTransactionOptions {
txParams: TransactionParams;
}
export class Xlm extends BaseCoin {
public readonly homeDomain: string;
public static readonly tokenPatternSeparator = '-'; // separator for token code and issuer
static readonly maxMemoId: string = '0xFFFFFFFFFFFFFFFF'; // max unsigned 64-bit number = 18446744073709551615
// max int64 number supported by the network (2^63)-1
// See: https://www.stellar.org/developers/guides/concepts/assets.html#amount-precision-and-representation
static readonly maxTrustlineLimit: string = '9223372036854775807';
constructor(bitgo: BitGoBase) {
super(bitgo);
this.homeDomain = 'bitgo.com'; // used for reverse federation lookup
}
static createInstance(bitgo: BitGoBase): BaseCoin {
return new Xlm(bitgo);
}
protected getStellarNetwork(): stellar.Networks {
return stellar.Networks.PUBLIC;
}
/**
* Factor between the base unit and its smallest subdivison
*/
getBaseFactor() {
return 1e7;
}
/**
* Identifier for the blockchain which supports this coin
*/
getChain(): string {
return 'xlm';
}
/**
* Identifier for the coin family
*/
getFamily(): string {
return 'xlm';
}
/**
* Complete human-readable name of this coin
*/
getFullName(): string {
return 'Stellar';
}
/**
* Url at which the stellar federation server can be reached
*/
getFederationServerUrl(): string {
return common.Environments[this.bitgo.getEnv()].stellarFederationServerUrl;
}
/**
* Url at which horizon can be reached
*/
getHorizonUrl(): string {
return 'https://horizon.stellar.org';
}
/** inheritdoc */
generateKeyPair(seed?: Buffer): KeyPair {
const keyPair = seed ? new StellarKeyPair({ seed }) : new StellarKeyPair();
const keys = keyPair.getKeys();
if (!keys.prv) {
throw new Error('Missing prv in key generation.');
}
return { pub: keys.pub, prv: keys.prv };
}
generateRootKeyPair(seed?: Buffer): KeyPair {
const keyPair = seed ? new StellarKeyPair({ seed }) : new StellarKeyPair();
const keys = keyPair.getKeys(true);
if (!keys.prv) {
throw new Error('Missing prv in key generation.');
}
return { prv: keys.prv + keys.pub, pub: keys.pub };
}
/**
* Get encoded ed25519 public key from raw data
*
* @param pub Raw public key
* @returns Encoded public key
*/
getPubFromRaw(pub: string): string {
return Utils.encodePublicKey(Buffer.from(pub, 'hex'));
}
/**
* Get encoded ed25519 private key from raw data
*
* @param prv Raw private key
* @returns Encoded private key
*/
getPrvFromRaw(prv: string): string {
return Utils.encodePrivateKey(Buffer.from(prv, 'hex'));
}
/**
* Return boolean indicating whether input is valid public key for the coin.
*
* @param pub the pub to be checked
* @returns is it valid?
*/
isValidPub(pub: string): boolean {
// Stellar's validation method only allows keys in Stellar-specific format, with a 'G' prefix
// We need to allow for both Stellar and raw root keys
return Utils.isValidRootPublicKey(pub) || Utils.isValidStellarPublicKey(pub);
}
/**
* Return boolean indicating whether input is valid private key for the coin
*
* @param prv the prv to be checked
* @returns is it valid?
*/
isValidPrv(prv: string): boolean {
// Stellar's validation method only allows keys in Stellar-specific format, with an 'S' prefix
// We need to allow for both Stellar and raw root private keys
return Utils.isValidRootPrivateKey(prv) || Utils.isValidStellarPrivateKey(prv);
}
/**
* Return boolean indicating whether a memo id is valid
*
* @param memoId memo id
* @returns true if memo id is valid
*/
isValidMemoId(memoId: string): boolean {
let memoIdNumber;
try {
stellar.Memo.id(memoId); // throws if the value is not valid memo id
memoIdNumber = new BigNumber(memoId);
} catch (e) {
return false;
}
return memoIdNumber.gte(0) && memoIdNumber.lt(Xlm.maxMemoId);
}
supportsDeriveKeyWithSeed(): boolean {
return false;
}
/** {@inheritDoc } **/
supportsMultisig(): boolean {
return true;
}
/** inherited doc */
getDefaultMultisigType(): MultisigType {
return multisigTypes.onchain;
}
/**
* Evaluates whether a memo is valid
*
* @param value value of the memo
* @param type type of the memo
* @returns true if value and type are a valid
*/
isValidMemo({ value, type }: Memo): boolean {
if (!value || !type) {
return false;
}
try {
// throws if the value is not valid for the type
// valid types are: 'id', 'text', 'hash', 'return'
// See https://www.stellar.org/developers/guides/concepts/transactions.html#memo
stellar.Memo[type](value);
} catch (e) {
return false;
}
return true;
}
/**
* Create instance of stellar.MuxedAccount from M address
* See: https://developers.stellar.org/docs/glossary/muxed-accounts
*/
getMuxedAccount(address: string): stellar.MuxedAccount {
try {
return stellar.MuxedAccount.fromAddress(address, '0');
} catch (e) {
throw new Error(`invalid muxed address: ${address}`);
}
}
/**
* Return boolean indicating whether a muxed address is valid
* See: https://developers.stellar.org/docs/glossary/muxed-accounts
*
* @param address
* @returns {boolean}
*/
isValidMuxedAddress(address: string): boolean {
if (!_.isString(address) || !address.startsWith('M')) {
return false;
}
try {
// return true if muxed account is valid or throw
return !!stellar.MuxedAccount.fromAddress(address, '0');
} catch (e) {
return false;
}
}
/**
* Minimum balance of a 2-of-3 multisig wallet
* @returns minimum balance in stroops
*/
async getMinimumReserve(): Promise<number> {
const server = new stellar.Server(this.getHorizonUrl());
const horizonLedgerInfo = await server.ledgers().order('desc').limit(1).call();
if (!horizonLedgerInfo) {
throw new Error('unable to connect to Horizon for reserve requirement data');
}
const baseReserve = horizonLedgerInfo.records[0].base_reserve_in_stroops;
// 2-of-3 wallets have a minimum reserve of 5x the base reserve
return 5 * baseReserve;
}
/**
* Transaction fee for each operation
* @returns transaction fee in stroops
*/
async getBaseTransactionFee(): Promise<number> {
const server = new stellar.Server(this.getHorizonUrl());
const horizonLedgerInfo = await server.ledgers().order('desc').limit(1).call();
if (!horizonLedgerInfo) {
throw new Error('unable to connect to Horizon for reserve requirement data');
}
return horizonLedgerInfo.records[0].base_fee_in_stroops;
}
/**
* Process address into address and memo id
*
* @param address the address
* @returns object containing address and memo id
*/
getAddressDetails(address: string): AddressDetails {
if (address.startsWith('M')) {
if (this.isValidMuxedAddress(address)) {
const muxedAccount = this.getMuxedAccount(address);
return {
baseAddress: muxedAccount.baseAccount().accountId(),
address,
id: muxedAccount.id(),
memoId: undefined,
};
} else {
throw new InvalidAddressError(`invalid muxed address: ${address}`);
}
}
const destinationDetails = url.parse(address);
const destinationAddress = destinationDetails.pathname || '';
if (!destinationAddress || !stellar.StrKey.isValidEd25519PublicKey(destinationAddress)) {
throw new Error(`invalid address: ${address}`);
}
// address doesn't have a memo id
if (destinationDetails.pathname === address) {
return {
baseAddress: address,
address: address,
id: undefined,
memoId: undefined,
};
}
if (!destinationDetails.query) {
throw new InvalidAddressError(`invalid address: ${address}`);
}
const queryDetails = querystring.parse(destinationDetails.query);
if (!queryDetails.memoId) {
// if there are more properties, the query details need to contain the memo id property
throw new InvalidAddressError(`invalid address: ${address}`);
}
if (Array.isArray(queryDetails.memoId)) {
throw new InvalidAddressError(
`memoId may only be given at most once, but found ${queryDetails.memoId.length} instances 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) || undefined;
if (!this.isValidMemoId(memoId)) {
throw new InvalidMemoIdError(`invalid address: '${address}', memoId is not valid`);
}
return {
baseAddress: destinationAddress,
address: destinationAddress,
id: undefined,
memoId,
};
}
/**
* Validate and return address with appended memo id or muxed address
*
* @param address address
* @param memoId memo id
* @returns address with memo id
*/
normalizeAddress({ address, memoId }: AddressDetails): string {
if (this.isValidMuxedAddress(address)) {
return address;
}
if (!stellar.StrKey.isValidEd25519PublicKey(address)) {
throw new Error(`invalid address details: ${address}`);
}
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 pub to be checked
* @returns is it valid?
*/
isValidAddress(address: string): boolean {
try {
const addressDetails = this.getAddressDetails(address);
return address === this.normalizeAddress(addressDetails);
} catch (e) {
return false;
}
}
/**
* Return a Stellar Asset in coin:token form (i.e. (t)xlm:<code>-<issuer>)
* If the asset is XLM, return the chain
* @param {stellar.Asset} asset - instance of Stellar Asset
*/
getTokenNameFromStellarAsset(asset: stellar.Asset): string {
const code = asset.getCode();
const issuer = asset.getIssuer();
if (asset.isNative()) {
return this.getChain();
}
return `${this.getChain()}${BaseCoin.coinTokenPatternSeparator}${code}${Xlm.tokenPatternSeparator}${issuer}`;
}
/**
* Evaluate whether a stellar username has valid format
* This method is used by the client when a stellar address is being added to a wallet
* Example of a common stellar username: foo@bar.baz
* The above example would result in the Stellar address: foo@bar.baz*bitgo.com
*
* @param username - stellar username
* @return true if stellar username is valid
*/
isValidStellarUsername(username: string): boolean {
return /^[a-z0-9\-_.+@]+$/.test(username);
}
/**
* Get an instance of FederationServer for BitGo lookups
*
* @returns instance of BitGo Federation Server
*/
getBitGoFederationServer(): stellar.FederationServer {
// Identify the URI scheme in case we need to allow connecting to HTTP server.
const isNonSecureEnv = !_.startsWith(common.Environments[this.bitgo.env].uri, 'https');
const federationServerOptions = { allowHttp: isNonSecureEnv };
return new stellar.FederationServer(this.getFederationServerUrl(), 'bitgo.com', federationServerOptions);
}
/**
* Perform federation lookups
* Our federation server handles lookups for bitgo as well as for other federation domains
*
* @param {String} [address] - address to look up
* @param {String} [accountId] - account id to look up
*/
private async federationLookup({
address,
accountId,
}: {
address?: string;
accountId?: string;
}): Promise<stellar.FederationServer.Record> {
try {
const federationServer = this.getBitGoFederationServer();
if (address) {
return await federationServer.resolveAddress(address);
} else if (accountId) {
return await federationServer.resolveAccountId(accountId);
} else {
throw new Error('invalid argument - must provide Stellar address or account id');
}
} catch (e) {
const error = _.get(e, 'response.data.detail');
if (error) {
throw new StellarFederationUserNotFoundError(error);
} else {
throw e;
}
}
}
/**
* Attempt to resolve a stellar address into a stellar account
*
* @param {String} address - stellar address to look for
*/
async federationLookupByName(address: string): Promise<stellar.FederationServer.Record> {
if (!address) {
throw new Error('invalid Stellar address');
}
return this.federationLookup({ address });
}
/**
* Attempt to resolve an account id into a stellar account
* Only works for accounts that can be resolved by our federation server
*
* @param {String} accountId - stellar account id
*/
async federationLookupByAccountId(accountId: string): Promise<stellar.FederationServer.Record> {
if (!accountId) {
throw new Error('invalid Stellar account');
}
return this.federationLookup({ accountId });
}
/**
* Check if address is a valid XLM address, and then make sure it matches the root address.
*
* @param address {String} the address to verify
* @param rootAddress {String} the wallet's root address
*/
async isWalletAddress({ address, rootAddress }: VerifyAddressOptions): Promise<boolean> {
if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}
const addressDetails = this.getAddressDetails(address);
const rootAddressDetails = this.getAddressDetails(rootAddress);
if (addressDetails.baseAddress !== rootAddressDetails.address) {
throw new UnexpectedAddressError(
`address validation failure: ${addressDetails.baseAddress} vs ${rootAddressDetails.address}`
);
}
return true;
}
/**
* Get extra parameters for prebuilding a tx
* Set empty recipients array in trustline txs
*/
async getExtraPrebuildParams(buildParams: ExtraPrebuildParamsOptions): Promise<BuildOptions> {
const params: { recipients?: Record<string, string>[] } = {};
if (buildParams.type === 'trustline') {
params.recipients = [];
}
return params;
}
/**
* @deprecated
*/
initiateRecovery(params: RecoveryOptions): never {
throw new Error('deprecated method');
}
/**
* Builds a funds recovery transaction without BitGo
* @param params
* - userKey: [encrypted] Stellar private key
* - backupKey: [encrypted] Stellar private key, or public key if the private key is held by a KRS provider
* - walletPassphrase: necessary if one of the private keys is encrypted
* - rootAddress: base address of the wallet to recover funds from
* - krsProvider: necessary if backup key is held by KRS
* - recoveryDestination: target address to send recovered funds to
*/
async recover(params: RecoveryOptions): Promise<RecoveryTransaction> {
// Check if unencrypted root keys were provided, convert to Stellar format if necessary
if (Utils.isValidRootPrivateKey(params.userKey)) {
params.userKey = Utils.encodePrivateKey(Buffer.from(params.userKey.slice(0, 64), 'hex'));
} else if (Utils.isValidRootPublicKey(params.userKey)) {
params.userKey = Utils.encodePublicKey(Buffer.from(params.userKey, 'hex'));
}
if (Utils.isValidRootPrivateKey(params.backupKey)) {
params.backupKey = Utils.encodePrivateKey(Buffer.from(params.backupKey.slice(0, 64), 'hex'));
} else if (Utils.isValidRootPublicKey(params.backupKey)) {
params.backupKey = Utils.encodePublicKey(Buffer.from(params.backupKey, 'hex'));
}
// Stellar's Ed25519 public keys start with a G, while private keys start with an S
const isKrsRecovery = params.backupKey.startsWith('G') && !params.userKey.startsWith('G');
const isUnsignedSweep = params.backupKey.startsWith('G') && params.userKey.startsWith('G');
if (isKrsRecovery) {
checkKrsProvider(this, params.krsProvider);
}
if (!this.isValidAddress(params.recoveryDestination)) {
throw new InvalidAddressError('Invalid destination address!');
}
const [userKey, backupKey] = getStellarKeys(this.bitgo, params);
if (!params.rootAddress || !stellar.StrKey.isValidEd25519PublicKey(params.rootAddress)) {
throw new Error(`Invalid wallet address: ${params.rootAddress}`);
}
const accountDataUrl = `${this.getHorizonUrl()}/accounts/${params.rootAddress}`;
const destinationUrl = `${this.getHorizonUrl()}/accounts/${params.recoveryDestination}`;
let accountData;
try {
accountData = await toBitgoRequest(request.get(accountDataUrl)).result();
} catch (e) {
throw new Error('Unable to reach the Stellar network via Horizon.');
}
// Now check if the destination account is empty or not
let unfundedDestination = false;
try {
await request.get(destinationUrl);
} catch (e) {
if (e.status === 404) {
// If the destination account does not yet exist, horizon responds with 404
unfundedDestination = true;
}
}
if (!accountData.sequence || !accountData.balances) {
throw new Error('Horizon server error - unable to retrieve sequence ID or account balance');
}
const account = new stellar.Account(params.rootAddress, accountData.sequence);
// Stellar supports multiple assets on chain, we're only interested in the balances entry whose type is "native" (XLM)
const nativeBalanceInfo = accountData.balances.find((assetBalance) => assetBalance['asset_type'] === 'native');
if (!nativeBalanceInfo) {
throw new Error('Provided wallet has a balance of 0 XLM, recovery aborted');
}
const walletBalance = Number(this.bigUnitsToBaseUnits(nativeBalanceInfo.balance));
const minimumReserve = await this.getMinimumReserve();
const baseTxFee = await this.getBaseTransactionFee();
const recoveryAmount = walletBalance - minimumReserve - baseTxFee;
const formattedRecoveryAmount = this.baseUnitsToBigUnits(recoveryAmount).toString();
const txBuilder = new stellar.TransactionBuilder(account, {
fee: baseTxFee.toFixed(0),
networkPassphrase: this.getStellarNetwork(),
});
const operation = unfundedDestination
? // In this case, we need to create the account
stellar.Operation.createAccount({
destination: params.recoveryDestination,
startingBalance: formattedRecoveryAmount,
})
: // Otherwise if the account already exists, we do a normal send
stellar.Operation.payment({
destination: params.recoveryDestination,
asset: stellar.Asset.native(),
amount: formattedRecoveryAmount,
});
const tx = txBuilder.addOperation(operation).setTimeout(stellar.TimeoutInfinite).build();
const feeInfo = {
fee: new BigNumber(tx.fee).toNumber(),
feeString: tx.fee,
};
if (!isUnsignedSweep) {
tx.sign(userKey);
}
if (!isKrsRecovery && !isUnsignedSweep) {
tx.sign(backupKey);
}
const transaction: RecoveryTransaction = {
txBase64: Xlm.txToString(tx),
recoveryAmount,
};
if (isKrsRecovery) {
transaction.backupKey = params.backupKey;
}
transaction.coin = this.getChain();
transaction.feeInfo = feeInfo;
return transaction;
}
/**
* 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<HalfSignedTransaction>}
*/
async signTransaction(params: SignTransactionOptions): Promise<HalfSignedTransaction> {
const { txPrebuild, prv } = params;
if (_.isUndefined(txPrebuild)) {
throw new Error('missing txPrebuild parameter');
}
if (!_.isObject(txPrebuild)) {
throw new Error(`txPrebuild must be an object, got type ${typeof txPrebuild}`);
}
if (_.isUndefined(prv)) {
throw new Error('missing prv parameter to sign transaction');
}
if (!_.isString(prv)) {
throw new Error(`prv must be a string, got type ${typeof prv}`);
}
const keyPair = Utils.createStellarKeypairFromPrv(prv);
const tx = new stellar.Transaction(txPrebuild.txBase64, this.getStellarNetwork());
tx.sign(keyPair);
const txBase64 = Xlm.txToString(tx);
const type = txPrebuild?.buildParams?.type;
const recipients = txPrebuild?.buildParams?.recipients;
if (type === 'enabletoken') {
return {
halfSigned: { txBase64 },
type,
recipients,
};
} else {
return { halfSigned: { txBase64 } };
}
}
/**
* Extend walletParams with extra params required for generating an XLM wallet
*
* Stellar wallets have three keychains on them. Two are generated by the platform, and the last is generated by the user.
* Initially, we need a root prv to generate the account, which must be distinct from all three keychains on the wallet.
* If a root prv is not provided, a random one is generated.
*/
async supplementGenerateWallet(
walletParams: SupplementGenerateWalletOptions
): Promise<SupplementGenerateWalletOptions> {
let seed;
const rootPrv = walletParams.rootPrivateKey;
if (rootPrv) {
if (!this.isValidPrv(rootPrv)) {
throw new Error('rootPrivateKey needs to be valid ed25519 secret seed');
}
seed = stellar.StrKey.decodeEd25519SecretSeed(rootPrv);
}
const keyPair = this.generateKeyPair(seed);
// extend the wallet initialization params
walletParams.rootPrivateKey = keyPair.prv;
return walletParams;
}
/**
* Sign message with private key
*
* @param key
* @param message
*/
async signMessage(key: KeyPair, message: string | Buffer): Promise<Buffer> {
if (!this.isValidPrv(key.prv)) {
throw new Error(`invalid prv: ${key.prv}`);
}
if (!Buffer.isBuffer(message)) {
message = Buffer.from(message);
}
const keypair = Utils.createStellarKeypairFromPrv(key.prv);
return keypair.sign(message);
}
/**
* Verifies if signature for message is valid.
*
* @param pub public key
* @param message signed message
* @param signature signature to verify
* @returns true if signature is valid.
*/
verifySignature(pub: string, message: string | Buffer, signature: Buffer) {
if (!this.isValidPub(pub)) {
throw new Error(`invalid pub: ${pub}`);
}
if (!Buffer.isBuffer(message)) {
message = Buffer.from(message);
}
const keyPair = Utils.createStellarKeypairFromPub(pub);
return keyPair.verify(message, signature);
}
/**
* Explain/parse transaction
* @param params
*/
async explainTransaction(params: ExplainTransactionOptions): Promise<TransactionExplanation> {
const { txHex, txBase64 } = params;
let tx: stellar.Transaction | undefined = undefined;
if (!txHex && !txBase64) {
throw new Error('explainTransaction missing txHex or txBase64 parameter, must have at least one');
}
try {
if (txHex) {
tx = new stellar.Transaction(Buffer.from(txHex, 'hex').toString('base64'), this.getStellarNetwork());
} else if (txBase64) {
tx = new stellar.Transaction(txBase64, this.getStellarNetwork());
}
} catch (e) {
throw new Error('txBase64 needs to be a valid tx encoded as base64 string');
}
if (!tx) {
throw new Error('tx needs to be defined in order to explain transaction');
}
const id = tx.hash().toString('hex');
// In a Stellar tx, the _memo property is an object with the methods:
// value() and arm() that provide memo value and type, respectively.
const memo: TransactionMemo =
_.result(tx, '_memo.value') && _.result(tx, '_memo.arm')
? {
value: (_.result(tx, '_memo.value') as any).toString(),
type: _.result(tx, '_memo.arm'),
}
: {};
let spendAmount = new BigNumber(0); // amount of XLM used in XLM-only txs
const spendAmounts = {}; // track both xlm and token amounts
if (_.isEmpty(tx.operations)) {
throw new Error('missing operations');
}
const outputs: TransactionOutput[] = [];
const operations: TransactionOperation[] = []; // non-payment operations
_.forEach(tx.operations, (op: stellar.Operation) => {
if (op.type === 'createAccount' || op.type === 'payment') {
// TODO Remove memoId from address
// Get memo to attach to address, if type is 'id'
const memoId = _.get(memo, 'type') === 'id' && !_.get(memo, 'value') ? `?memoId=${memo.value}` : '';
let asset;
if (op.type === 'payment') {
if (op.asset.getAssetType() === 'liquidity_pool_shares') {
throw new Error('Invalid asset type');
}
asset = op.asset as stellar.Asset;
} else {
asset = stellar.Asset.native();
}
const coin = this.getTokenNameFromStellarAsset(asset); // coin or token id
const output: TransactionOutput = {
amount: this.bigUnitsToBaseUnits(
(op as stellar.Operation.CreateAccount).startingBalance || (op as stellar.Operation.Payment).amount
),
address: op.destination + memoId,
coin,
};
if (!_.isUndefined(spendAmounts[coin])) {
spendAmounts[coin] = spendAmounts[coin].plus(output.amount);
} else {
spendAmounts[coin] = new BigNumber(output.amount);
}
if (asset.isNative()) {
spendAmount = spendAmount.plus(output.amount);
}
outputs.push(output);
} else if (op.type === 'changeTrust') {
if (op.line.getAssetType() === 'liquidity_pool_shares') {
throw new Error('Invalid asset type');
}
const asset = op.line as stellar.Asset;
operations.push({
type: op.type,
coin: this.getTokenNameFromStellarAsset(asset),
asset,
limit: this.bigUnitsToBaseUnits(op.limit),
});
}
});
const outputAmount = spendAmount.toFixed(0);
const outputAmounts = _.mapValues(spendAmounts, (amount: BigNumber) => amount.toFixed(0));
const fee = {
fee: new BigNumber(tx.fee).toFixed(0),
feeRate: null,
size: null,
};
return {
displayOrder: [
'id',
'outputAmount',
'outputAmounts',
'changeAmount',
'outputs',
'changeOutputs',
'fee',
'memo',
'operations',
],
id,
outputs,
outputAmount,
outputAmounts,
changeOutputs: [],
changeAmount: '0',
memo,
fee,
operations,
} as any;
}
/**
* Verify that a tx prebuild's operations comply with the original intention
* @param {stellar.Operation} operations - tx operations
* @param {TransactionParams} txParams - params used to build the tx
*/
verifyEnableTokenTxOperations(operations: stellar.Operation[], txParams: TransactionParams): void {
const trustlineOperations = _.filter(operations, ['type', 'changeTrust']) as stellar.Operation.ChangeTrust[];
if (trustlineOperations.length !== _.get(txParams, 'recipients', []).length) {
throw new Error('transaction prebuild does not match expected trustline operations');
}
_.forEach(trustlineOperations, (op: stellar.Operation) => {
if (op.type !== 'changeTrust') {
throw new Error('Invalid asset type');
}
if (op.line.getAssetType() === 'liquidity_pool_shares') {
throw new Error('Invalid asset type');
}
const asset = op.line as stellar.Asset;
const opToken = this.getTokenNameFromStellarAsset(asset);
const tokenTrustline = _.find(txParams.recipients, (recipient) => {
// trustline params use limits in base units
const opLimitBaseUnits = this.bigUnitsToBaseUnits(op.limit);
// Enable token limit is set to Xlm.maxTrustlineLimit by default
return recipient.tokenName === opToken && opLimitBaseUnits === Xlm.maxTrustlineLimit;
});
if (!tokenTrustline) {
throw new Error('transaction prebuild does not match expected trustline tokens');
}
});
}
/**
* Verify that a tx prebuild's operations comply with the original intention
* @param {stellar.Operation} operations - tx operations
* @param {TransactionParams} txParams - params used to build the tx
*/
verifyTrustlineTxOperations(operations: stellar.Operation[], txParams: TransactionParams): void {
const trustlineOperations = _.filter(operations, ['type', 'changeTrust']) as stellar.Operation.ChangeTrust[];
if (trustlineOperations.length !== _.get(txParams, 'trustlines', []).length) {
throw new Error('transaction prebuild does not match expected trustline operations');
}
_.forEach(trustlineOperations, (op: stellar.Operation) => {
if (op.type !== 'changeTrust') {
throw new Error('Invalid asset type');
}
if (op.line.getAssetType() === 'liquidity_pool_shares') {
throw new Error('Invalid asset type');
}
const asset = op.line as stellar.Asset;
const opToken = this.getTokenNameFromStellarAsset(asset);
const tokenTrustline = _.find(txParams.trustlines, (trustline) => {
// trustline params use limits in base units
const opLimitBaseUnits = this.bigUnitsToBaseUnits(op.limit);
// Prepare the conditions to check for
// Limit will always be set in the operation, even if it was omitted from txParams in the following cases:
// 1. Action is 'add' - limit is set to Xlm.maxTrustlineLimit by default
// 2. Action is 'remove' - limit is set to '0'
const noLimit = _.isUndefined(trustline.limit);
const addTrustlineWithDefaultLimit = trustline.action === 'add' && opLimitBaseUnits === Xlm.maxTrustlineLimit;
const removeTrustline = trustline.action === 'remove' && opLimitBaseUnits === '0';
return (
trustline.token === opToken &&
(trustline.limit === opLimitBaseUnits || (noLimit && (addTrustlineWithDefaultLimit || removeTrustline)))
);
});
if (!tokenTrustline) {
throw new Error('transaction prebuild does not match expected trustline tokens');
}
});
}
/**
* Verify that a transaction prebuild complies with the original intention
*
* @param options
* @param options.txPrebuild prebuild object returned by platform
* @param options.txPrebuild.txBase64 prebuilt transaction encoded as base64 string
* @param options.wallet wallet object to obtain keys to verify against
* @param options.verification specifying some verification parameters
* @param options.verification.disableNetworking Disallow fetching any data from the internet for verification purposes
* @param options.verification.keychains Pass keychains manually rather than fetching them by id
*/
async verifyTransaction(options: VerifyTransactionOptions): Promise<boolean> {
// TODO BG-5600 Add parseTransaction / improve verification
const { txParams, txPrebuild, wallet, verification = {} } = options;
const disableNetworking = !!verification.disableNetworking;
if (!txPrebuild.txBase64) {
throw new Error('missing required tx prebuild property txBase64');
}
const tx = new stellar.Transaction(txPrebuild.txBase64, this.getStellarNetwork());
if (txParams.recipients && txParams.recipients.length > 1) {
throw new Error('cannot specify more than 1 recipient');
}
// Stellar txs are made up of operations. We only care about Create Account and Payment for sending funds.
const outputOperations = _.filter(
tx.operations,
(operation) => operation.type === 'createAccount' || operation.type === 'payment'
);
if (txParams.type === 'enabletoken') {
this.verifyEnableTokenTxOperations(tx.operations, txParams);
} else if (txParams.type === 'trustline') {
this.verifyTrustlineTxOperations(tx.operations, txParams);
} else {
if (_.isEmpty(outputOperations)) {
throw new Error('transaction prebuild does not have any operations');
}
_.forEach(txParams.recipients, (expectedOutput, index) => {
const expectedOutputAddressDetails = this.getAddressDetails(expectedOutput.address);
const expectedOutputAddress = expectedOutputAddressDetails.address;
const output = outputOperations[index] as stellar.Operation.Payment | stellar.Operation.CreateAccount;
if (output.destination !== expectedOutputAddress) {
throw new Error('transaction prebuild does not match expected recipient');
}
const expectedOutputAmount = new BigNumber(expectedOutput.amount);
// The output amount is expressed as startingBalance in createAccount operations and as amount in payment operations.
const outputAmountString = output.type === 'createAccount' ? output.startingBalance : output.amount;
const outputAmount = new BigNumber(this.bigUnitsToBaseUnits(outputAmountString));
if (!outputAmount.eq(expectedOutputAmount)) {
throw new Error('transaction prebuild does not match expected amount');
}
});
}
// Verify the user signature, if the tx is half-signed
if (!_.isEmpty(tx.signatures)) {
const userSignature = tx.signatures[0].signature();
// obtain the keychains and key signatures
let keychains = verification.keychains;
if (!keychains && disableNetworking) {
throw new Error('cannot fetch keychains without networking');
} else if (!keychains) {
keychains = await promiseProps({
user: this.keychains().get({ id: wallet.keyIds()[KeyIndices.USER] }),
backup: this.keychains().get({ id: wallet.keyIds()[KeyIndices.BACKUP] }),
});
}
if (!keychains || !keychains.backup || !keychains.user) {
throw new Error('keychains are required, but could not be fetched');
}
assert(keychains.backup.pub);
if (this.verifySignature(keychains.backup.pub, tx.hash(), userSignature)) {
throw new Error('transaction signed with wrong key');
}
assert(keychains.user.pub);
if (!this.verifySignature(keychains.user.pub, tx.hash(), userSignature)) {
throw new Error('transaction signature invalid');
}
}
return true;
}
/** inheritdoc */
deriveKeyWithSeed(): { derivationPath: string; key: string } {
throw new NotSupported('method deriveKeyWithSeed not supported for eddsa curve');
}
/**
* stellar-sdk has two overloads for toXDR, and typescript can't seem to figure out the
* correct one to use, so we have to be very explicit as to which one we want.
* @param tx transaction to convert
*/
protected static txToString = (tx: stellar.Transaction): string =>
(tx.toEnvelope().toXDR as (_: string) => string)('base64');
async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
return {};
}
/**
* Gets config for how token enablements work for this coin
* @returns
* requiresTokenEnablement: True if tokens need to be enabled for this coin
* supportsMultipleTokenEnablements: True if multiple tokens can be enabled in one transaction
*/
getTokenEnablementConfig(): TokenEnablementConfig {
return {
requiresTokenEnablement: true,
supportsMultipleTokenEnablements: false,
};
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!