can not sign input with my keypair
MovsesDev opened this issue · comments
MovsesDev commented
import { BuildTransactionOptions } from './common';
import { getEnvConfig } from 'src/config';
import * as bitcoin from 'bitcoinjs-lib';
import { Network } from 'bitcoinjs-lib';
import ECPairFactory from 'ecpair';
import * as ecc from '@bitcoin-js/tiny-secp256k1-asmjs';
const ECPair = ECPairFactory(ecc);
import axios, { Axios } from 'axios';
import pLimit from 'p-limit';
import BigNumber from 'bignumber.js';
import { FeeType } from 'src/store/settings/settings.slice';
import { bitcoinApi } from 'src/api/bitcoin';
import { BlockchainProvider } from './interface/blockchainProvider';
import {
EstimateEthereumFeeRequest,
EstimateEthereumFeeResponse,
} from 'definitions/generated/wallet/tatum/ethereum/v1/ethereum';
import { EstimateTronFeeResponse } from 'definitions/generated/wallet/tatum/tron/v1/tron';
import * as assert from 'assert';
const blockStreamLimit = pLimit(3);
type IUtxo = any;
type IBitcoinNetworkFee = any;
type IUtxoBlockStreamResponse = any;
type BitcoinFeeRecomendedResponse = any;
type IBuildBitcoinTransactionDataArgs = {
from: string;
to: string;
fee: FeeType;
amount: string;
feeValue: any;
};
type IBitcoinTransactionData = any;
const DEFAULT_OUTPUT_LENGTH = 2;
const INPUT_SIZE_BYTES = 148;
const OUTPUT_SIZE_BYTES = 32;
const ADDITIONAL_SIZE_BYTES = 10;
const sortUTXO = (a: IUtxo, b: IUtxo): number => {
const aAmount = Number(a.satoshiAmount);
const bAmount = Number(b.satoshiAmount);
return bAmount - aAmount;
};
export class BitcoinService implements BlockchainProvider {
private readonly blockStreamInstance: Axios;
private readonly memPoolInstance: Axios;
private readonly isTestnet: boolean;
private readonly network: Network;
constructor() {
this.isTestnet = !getEnvConfig().isMainnet;
this.network = this.isTestnet
? bitcoin.networks.testnet
: bitcoin.networks.bitcoin;
this.blockStreamInstance = axios.create({
baseURL: `https://blockstream.info${
this.isTestnet ? '/testnet' : ''
}/api`,
});
this.memPoolInstance = axios.create({
baseURL: 'https://mempool.space/api',
});
}
getTransactions = (address: string) => {
return bitcoinApi
.getTransactions({ address })
.then(res => res)
.catch(() => {
// Hack for backend not found error
return {
transactions: [],
};
});
};
getTransaction = (hash: string) => {
return bitcoinApi.getTransaction({ hash });
};
async getBalance(address: string) {
const res = await bitcoinApi.getBalance({ address });
return res.result;
}
feeAdapter = (
res: EstimateTronFeeResponse | EstimateEthereumFeeResponse,
decimals: number,
) => {
return {
estimateFees: {
slow: BigNumber(res.result.estimations.safe).dividedBy(100_000_000),
standard: BigNumber(res.result.estimations.standard).dividedBy(
100_000_000,
),
fast: BigNumber(res.result.estimations.fast).dividedBy(100_000_000),
},
};
};
getContractData = (address: string) => {
return bitcoinApi.getContractInfo({ address }).then(res => res.result);
};
estimateFee = (data: EstimateEthereumFeeRequest) => {
console.log(data);
return bitcoinApi.estimateFee({
// ...data,
// to: data.contractAddress === 'native' ? data.to : data.contractAddress,
contractAddress:
data.contractAddress === 'native' ? undefined : data.contractAddress,
amount: data.amount,
from: data.from,
});
};
public async sendTransaction(txOptions: BuildTransactionOptions, pk: string) {
try {
const estimatedFee = await this.estimateFee({
to: txOptions.to,
from: txOptions.from,
amount: txOptions.value,
});
const feeValue = this.feeAdapter(estimatedFee, 8).estimateFees[
txOptions.feeType
];
const txBuildParams = await this.buildTransactionParams({
from: txOptions.from,
to: txOptions.to,
fee: txOptions.feeType,
amount: txOptions.value,
feeValue,
});
console.log('txOptions1', txOptions);
console.log('txOptions2', txBuildParams);
console.log('feeValue', Number(feeValue));
console.log('feeValue', feeValue);
const psbt = new bitcoin.Psbt({ network: this.network });
console.log('index', txBuildParams.utxo[0].index);
psbt.addInput({
hash: txBuildParams.utxo[0].txId,
index: txBuildParams.utxo[0].index,
witnessUtxo: {
script: Buffer.from(txBuildParams.scriptPubKey, 'hex'),
value: Number(this.toSatoshi(txBuildParams.total)),
},
});
psbt.addOutput({
address: txBuildParams.to,
value: Number(
this.toSatoshi(BigNumber(txBuildParams.total).minus(feeValue)),
),
});
console.log(
'value',
Number(this.toSatoshi(BigNumber(txBuildParams.total).minus(feeValue))),
);
console.log('keyPair', pk);
const keyPair = ECPair.fromPrivateKey(Buffer.from(pk, 'hex'), {
network: this.network,
});
console.log('keyPair', keyPair);
psbt.signInput(0, keyPair);
psbt.finalizeAllInputs();
const tx = psbt.extractTransaction();
return bitcoinApi.broadcast({ data: tx.toHex() });
} catch (e) {
console.log('AAA', e);
}
}
public async buildTransactionParams({
from,
to,
amount,
fee = 'slow',
feeValue,
}: IBuildBitcoinTransactionDataArgs): Promise<IBitcoinTransactionData> {
const allUtxo = await this.getAllAddressUtxos(from);
const rawTxs = await this.getTXS(from);
return {
from,
to,
scriptPubKey: rawTxs[0].vin[0].prevout.scriptpubkey,
...this.estimateTransactionFeeByUtxo(allUtxo, amount, feeValue),
};
}
private estimateTransactionFeeByUtxo(
utxo: IUtxo[],
amount: string,
feeValue: BigNumber,
) {
const value = new BigNumber(this.toSatoshi(amount));
const result = utxo.reduce(
(acc, item) => {
const { total, inputLength } = acc;
// const currentSatoshiFee = this.calculateFee({ inputLength, feeRate });
// acc.currentSatoshiFee = currentSatoshiFee;
if (total.lt(value.plus(feeValue))) {
acc.utxo.push(item);
acc.total = total.plus(item.satoshiAmount);
acc.inputLength = inputLength + 1;
}
return acc;
},
{
utxo: [] as IUtxo[],
total: new BigNumber(0),
inputLength: 1,
currentSatoshiFee: new BigNumber(0),
},
);
if (result.total.lt(value.plus(result.currentSatoshiFee))) {
const unspent = this.toBtc(result.total);
const need = this.toBtc(value.plus(result.currentSatoshiFee));
throw new Error(
`INSUFFICIENT_BALANCE. Unspent - ${unspent}, need - ${need}`,
);
}
return {
utxo: result.utxo,
total: this.toBtc(result.total),
fee: this.toBtc(result.currentSatoshiFee),
amount,
};
}
public calculateFee(args: {
inputLength: number;
feeRate: BigNumber;
outputLength?: number;
}): BigNumber {
const { inputLength, feeRate, outputLength = DEFAULT_OUTPUT_LENGTH } = args;
const inputSize = new BigNumber(inputLength).multipliedBy(INPUT_SIZE_BYTES);
const outputSize = new BigNumber(outputLength).multipliedBy(
OUTPUT_SIZE_BYTES,
);
const otherBytes = new BigNumber(ADDITIONAL_SIZE_BYTES).plus(inputLength);
return inputSize.plus(outputSize).plus(otherBytes).multipliedBy(feeRate);
}
private async getAllAddressUtxos(from: string): Promise<IUtxo[]> {
const rawUtxo = await this.getUTXO(from);
console.log('rawutxo', rawUtxo);
if (!rawUtxo.length) {
throw new Error('Insufficent balance');
}
return rawUtxo
.map(item => ({
txId: item.txid,
satoshiAmount: new BigNumber(item.value).toString(),
index: item.vout,
}))
.sort(sortUTXO);
}
public async getNetworkFee() {
return this.memPoolInstance
.get<BitcoinFeeRecomendedResponse>('/v1/fees/recommended')
.then(r => ({
slow: r.data.economyFee,
standard: r.data.hourFee,
fast: r.data.fastestFee,
}));
}
public getUTXO(address: string): Promise<any[]> {
return blockStreamLimit(() =>
this.blockStreamInstance
.get<any[]>(`/address/${address}/utxo`)
.then(r => r.data),
);
}
public getTXS(address: string): Promise<IUtxoBlockStreamResponse[]> {
return blockStreamLimit(() =>
this.blockStreamInstance
.get<IUtxoBlockStreamResponse[]>(`/address/${address}/txs`)
.then(r => r.data),
);
}
public toBtc(val: string | number | BigNumber): string {
return new BigNumber(val).dividedBy(100_000_000).toString();
}
public toSatoshi(val: string | number | BigNumber): string {
return new BigNumber(val).multipliedBy(100_000_000).toString();
}
}
export default new BitcoinService();
Jonathan Underwood commented
txOptions.from
and pk
are not referring to the same keypair.
Jonathan Underwood commented
Actually: scriptPubKey: rawTxs[0].vin[0].prevout.scriptpubkey,
this is the part that's wrong.
You are assuming that the first input of the first tx returned by the API MUST be the same key.
There is no guarantee that every input and every output of the transactions returned by the txs endpoint are the same key.
MovsesDev commented
so what should i do?
MovsesDev commented
what value has to be for scriptPubKey?
Jonathan Underwood commented
The output script associated with the from
address.