meta-contract-debug
Version:
Meta Contract SDK
988 lines • 65.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FtManager = exports.sighashType = void 0;
const scryptlib_1 = require("../scryptlib");
const error_1 = require("../common/error");
const mvc = require("../mvc");
const __1 = require("..");
const constants_1 = require("./constants");
const BN = require("../bn.js");
const TokenUtil = require("../common/tokenUtil");
const $ = require("../common/argumentCheck");
const Prevouts_1 = require("../common/Prevouts");
const tx_composer_1 = require("../tx-composer");
const token_1 = require("./contract-factory/token");
const contractUtil_1 = require("./contractUtil");
const utils_1 = require("../common/utils");
const tokenGenesis_1 = require("./contract-factory/tokenGenesis");
const tokenTransferCheck_1 = require("./contract-factory/tokenTransferCheck");
const ftProto = require("./contract-proto/token.proto");
const DustCalculator_1 = require("../common/DustCalculator");
const SizeTransaction_1 = require("../common/SizeTransaction");
const transactionHelpers_1 = require("../helpers/transactionHelpers");
const contractHelpers_1 = require("../helpers/contractHelpers");
const dummy_1 = require("../common/dummy");
const protoheader_1 = require("../common/protoheader");
const jsonDescr = require('./contract-desc/txUtil_desc.json');
const { TxInputProof, TxOutputProof } = (0, scryptlib_1.buildTypeClasses)(jsonDescr);
const Signature = mvc.crypto.Signature;
const _ = mvc.deps._;
exports.sighashType = Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID;
contractUtil_1.ContractUtil.init();
function checkParamGenesis(genesis) {
$.checkArgument(_.isString(genesis), 'Invalid Argument: genesis should be a string');
$.checkArgument(genesis.length == 40, `Invalid Argument: genesis.length must be 40`);
}
function checkParamCodehash(codehash) {
$.checkArgument(_.isString(codehash), 'Invalid Argument: codehash should be a string');
$.checkArgument(codehash.length == 40, `Invalid Argument: codehash.length must be 40`);
$.checkArgument(codehash == contractUtil_1.ContractUtil.tokenCodeHash ||
codehash == contractUtil_1.ContractUtil.tokenGenesisCodeHash ||
codehash === '57344f46cc0d0c8dfea7af3300b1b3a0f4216c04' ||
codehash === 'a2421f1e90c6048c36745edd44fad682e8644693', `a valid codehash should be ${contractUtil_1.ContractUtil.tokenCodeHash}, but the provided is ${codehash} `);
}
function checkParamReceivers(receivers) {
const ErrorName = 'ReceiversFormatError';
if ((0, utils_1.isNull)(receivers)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `${ErrorName}: param should not be null`);
}
if (receivers.length > 0) {
let receiver = receivers[0];
if ((0, utils_1.isNull)(receiver.address) || (0, utils_1.isNull)(receiver.amount)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `${ErrorName}-valid format example
[
{
address: "mtjjuRuA84b2qVyo28AyJQ8AoUmpbWEqs3",
amount: "1000",
},
]
`);
}
let amount = new BN(receiver.amount.toString());
if (amount.lten(0)) {
throw `receiver amount must greater than 0 but now is ${receiver.amount}`;
}
}
}
function parseSensibleID(sensibleID) {
let sensibleIDBuf = Buffer.from(sensibleID, 'hex');
let genesisTxId = sensibleIDBuf.slice(0, 32).reverse().toString('hex');
let genesisOutputIndex = sensibleIDBuf.readUIntLE(32, 4);
return {
genesisTxId,
genesisOutputIndex,
};
}
class FtManager {
constructor({ network = __1.API_NET.MAIN, apiTarget = __1.API_TARGET.MVC, purse, feeb = constants_1.FEEB, apiHost, dustLimitFactor = 300, dustAmount, debug = false, }) {
// 初始化API
this.network = network;
this._api = new __1.Api(network, apiTarget, apiHost);
// 初始化钱包
if (purse) {
const privateKey = mvc.PrivateKey.fromWIF(purse);
const address = privateKey.toAddress(network);
this.purse = {
privateKey,
address,
};
}
// 初始化零地址
this.zeroAddress = new mvc.Address('1111111111111111111114oLvT2');
this.dustCalculator = new DustCalculator_1.DustCalculator(dustLimitFactor, dustAmount);
this.transferCheckCodeHashArray = contractUtil_1.ContractUtil.transferCheckCodeHashArray;
this.unlockContractCodeHashArray = contractUtil_1.ContractUtil.unlockContractCodeHashArray;
// 初始化费率
this.feeb = feeb;
this.debug = false;
}
get api() {
return this._api;
}
get sensibleApi() {
return this._api;
}
/**
* Create a transaction for genesis
* @param tokenName token name, limited to 20 bytes
* @param tokenSymbol the token symbol, limited to 10 bytes
* @param decimalNum the decimal number, range 0-255
* @param utxos (Optional) specify mvc utxos
* @param changeAddress (Optional) specify mvc changeAddress
* @param opreturnData (Optional) append an opReturn output
* @param genesisWif the private key of the token genesiser
* @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false
* @returns
*/
genesis({ tokenName, tokenSymbol, decimalNum, utxos: utxosInput, changeAddress, opreturnData, genesisWif, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
// TODO 检查必要参数
// validate params
$.checkArgument(_.isString(tokenName) && Buffer.from(tokenName).length <= 40, `tokenName should be a string and not be larger than 40 bytes`);
$.checkArgument(_.isString(tokenSymbol) && Buffer.from(tokenSymbol).length <= 10, 'tokenSymbol should be a string and not be larger than 10 bytes');
$.checkArgument(_.isNumber(decimalNum) && decimalNum >= 0 && decimalNum <= 255, 'decimalNum should be a number and must be between 0 and 255');
const utxoInfo = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput);
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
const tokenAddress = genesisWif
? mvc.PrivateKey.fromWIF(genesisWif).toAddress(this.network)
: this.purse.address;
let { txComposer } = yield this._genesis({
tokenName,
tokenSymbol,
decimalNum,
utxos: utxoInfo.utxos,
utxoPrivateKeys: utxoInfo.utxoPrivateKeys,
changeAddress: changeAddress,
tokenAddress: tokenAddress.hashBuffer.toString('hex'),
opreturnData,
});
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.api.broadcast(txHex);
}
let { codehash, genesis, sensibleId } = (0, contractHelpers_1.getGenesisIdentifiers)({
genesisTx: txComposer.getTx(),
purse: { address: tokenAddress, privateKey: this.purse.privateKey },
transferCheckCodeHashArray: this.transferCheckCodeHashArray,
unlockContractCodeHashArray: this.unlockContractCodeHashArray,
type: 'ft',
});
return {
txHex,
txid: txComposer.getTxId(),
tx: txComposer.getTx(),
codehash,
genesis,
sensibleId,
};
});
}
issue(options) {
return __awaiter(this, void 0, void 0, function* () {
return this.mint(options);
});
}
mint({
// genesis,
// codehash,
sensibleId, genesisWif, receiverAddress, tokenAmount, allowIncreaseMints = true, utxos: utxosInput, changeAddress, opreturnData, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
// checkParamGenesis(genesis)
// checkParamCodehash(codehash)
$.checkArgument(sensibleId, 'sensibleId is required');
$.checkArgument(genesisWif, 'genesisWif is required');
$.checkArgument(receiverAddress, 'receiverAddress is required');
$.checkArgument(tokenAmount, 'tokenAmount is required');
const utxoInfo = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput);
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
let genesisPrivateKey = new mvc.PrivateKey(genesisWif);
let genesisPublicKey = genesisPrivateKey.toPublicKey();
receiverAddress = new mvc.Address(receiverAddress, this.network);
tokenAmount = new BN(tokenAmount.toString());
let { txComposer } = yield this._mint({
// genesis,
// codehash,
sensibleId,
receiverAddress,
tokenAmount,
allowIncreaseMints,
utxos: utxoInfo.utxos,
utxoPrivateKeys: utxoInfo.utxoPrivateKeys,
changeAddress,
opreturnData,
genesisPrivateKey,
genesisPublicKey,
});
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.api.broadcast(txHex);
}
return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx() };
});
}
_mint({
// genesis,
// codehash,
sensibleId, receiverAddress, tokenAmount, allowIncreaseMints = true, utxos, utxoPrivateKeys, changeAddress, opreturnData, genesisPrivateKey, genesisPublicKey, }) {
return __awaiter(this, void 0, void 0, function* () {
const genesisAddress = genesisPrivateKey.toAddress(this.network).toString();
let { genesisContract, genesisTxId, genesisOutputIndex, genesisUtxo } = yield this._prepareMintUtxo({ sensibleId, genesisAddress });
let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0);
let estimateSatoshis = yield this._calMintEstimateFee({
genesisUtxoSatoshis: genesisUtxo.satoshis,
opreturnData,
allowIncreaseMints,
utxoMaxCount: utxos.length,
});
if (balance < estimateSatoshis) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estimateSatoshis}, but only ${balance}.`);
}
let newGenesisContract = genesisContract.clone();
newGenesisContract.setFormatedDataPart({
sensibleID: {
txid: genesisTxId,
index: genesisOutputIndex,
},
});
let tokenContract = token_1.TokenFactory.createContract(this.transferCheckCodeHashArray, this.unlockContractCodeHashArray);
tokenContract.setFormatedDataPart(Object.assign({}, newGenesisContract.getFormatedDataPart(), {
tokenAddress: (0, scryptlib_1.toHex)(receiverAddress.hashBuffer),
tokenAmount,
genesisHash: newGenesisContract.getScriptHash(),
}));
const txComposer = new tx_composer_1.TxComposer();
const genesisInputIndex = (0, transactionHelpers_1.addContractInput)(txComposer, genesisUtxo, genesisPublicKey.toAddress(this.network).toString(), utils_1.CONTRACT_TYPE.BCP02_TOKEN_GENESIS);
const p2pkhInputIndexs = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos);
//If increase issues is allowed, add a new issue contract as the first output
let newGenesisOutputIndex = -1;
if (allowIncreaseMints) {
newGenesisOutputIndex = (0, transactionHelpers_1.addContractOutput)({
txComposer,
contract: newGenesisContract,
dustCalculator: this.dustCalculator,
});
}
const tokenOutputIndex = (0, transactionHelpers_1.addContractOutput)({
txComposer,
contract: tokenContract,
dustCalculator: this.dustCalculator,
});
//If there is opReturn, add it to the output
let opreturnScriptHex = '';
if (opreturnData) {
const opreturnOutputIndex = (0, transactionHelpers_1.addOpreturnOutput)(txComposer, opreturnData);
opreturnScriptHex = txComposer.getOutput(opreturnOutputIndex).script.toHex();
}
const prevInputIndex = 0; // TODO: 0?
const genesisTx = genesisUtxo.satotxInfo.tx;
const inputRes = TokenUtil.getTxInputProof(genesisTx, prevInputIndex);
const genesisTxInputProof = new TxInputProof(inputRes[0]);
const genesisTxHeader = inputRes[1]; // TODO:
// Find a valid preGenesisTx
const genesisTxInput = genesisTx.inputs[prevInputIndex];
const preGenesisOutputIndex = genesisTxInput.outputIndex;
const preGenesisTxId = genesisTxInput.prevTxId.toString('hex');
const preGenesisTxHex = yield this.api.getRawTxData(preGenesisTxId);
const preGenesisTx = new mvc.Transaction(preGenesisTxHex);
const prevOutputProof = TokenUtil.getTxOutputProof(preGenesisTx, preGenesisOutputIndex);
const pubKey = new scryptlib_1.PubKey(genesisPublicKey.toHex());
//The first round of calculations get the exact size of the final transaction, and then change again
//Due to the change, the script needs to be unlocked again in the second round
//let the fee to be exact in the second round
for (let c = 0; c < 2; c++) {
// TODO: 取消两轮?
txComposer.clearChangeOutput();
const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb);
let unlockResult = genesisContract.unlock({
txPreimage: txComposer.getInputPreimage(genesisInputIndex),
pubKey,
sig: new scryptlib_1.Sig(genesisPrivateKey
? (0, scryptlib_1.toHex)(txComposer.getTxFormatSig(genesisPrivateKey, genesisInputIndex))
: utils_1.PLACE_HOLDER_SIG),
tokenScript: new scryptlib_1.Bytes(txComposer.getOutput(tokenOutputIndex).script.toHex()),
// GenesisTx Input Proof
genesisTxHeader,
prevInputIndex,
genesisTxInputProof,
// Prev GenesisTx Output Proof
prevGenesisTxHeader: prevOutputProof.txHeader,
prevTxOutputHashProof: prevOutputProof.hashProof,
prevTxOutputSatoshiBytes: prevOutputProof.satoshiBytes,
genesisSatoshis: newGenesisOutputIndex != -1 ? txComposer.getOutput(newGenesisOutputIndex).satoshis : 0,
tokenSatoshis: txComposer.getOutput(tokenOutputIndex).satoshis,
changeSatoshis: changeOutputIndex != -1 ? txComposer.getOutput(changeOutputIndex).satoshis : 0,
changeAddress: new scryptlib_1.Ripemd160((0, scryptlib_1.toHex)(changeAddress.hashBuffer)),
opReturnScript: new scryptlib_1.Bytes(opreturnScriptHex),
});
// const txContext = {
// tx: txComposer.getTx(),
// inputIndex: 0,
// inputSatoshis: txComposer.getOutput(newGenesisOutputIndex).satoshis,
// }
// const verify = unlockResult.verify(txContext)
// console.log({ verify })
if (this.debug && genesisPrivateKey && c == 1) {
let ret = unlockResult.verify({
tx: txComposer.tx,
inputIndex: genesisInputIndex,
inputSatoshis: txComposer.getInput(genesisInputIndex).output.satoshis,
});
if (ret.success == false)
throw ret;
}
txComposer.getInput(genesisInputIndex).setScript(unlockResult.toScript());
}
(0, transactionHelpers_1.unlockP2PKHInputs)(txComposer, p2pkhInputIndexs, utxoPrivateKeys);
// if (utxoPrivateKeys && utxoPrivateKeys.length > 0) {
// p2pkhInputIndexs.forEach((inputIndex) => {
// let privateKey = utxoPrivateKeys.splice(0, 1)[0]
// txComposer.unlockP2PKHInput(privateKey, inputIndex)
// })
// }
(0, transactionHelpers_1.checkFeeRate)(txComposer, this.feeb);
return { txComposer };
});
}
_prepareMintUtxo({ sensibleId, genesisAddress, }) {
return __awaiter(this, void 0, void 0, function* () {
let genesisContract = tokenGenesis_1.TokenGenesisFactory.createContract();
//Looking for UTXO for issue
let { genesisTxId, genesisOutputIndex } = parseSensibleID(sensibleId);
let genesisUtxo = yield this._getMintUtxo(genesisContract.getCodeHash(), genesisTxId, genesisOutputIndex, genesisAddress);
if (!genesisUtxo) {
throw new error_1.CodeError(error_1.ErrCode.EC_FIXED_TOKEN_SUPPLY, 'token supply is fixed');
}
let txHex = yield this.api.getRawTxData(genesisUtxo.txId);
const tx = new mvc.Transaction(txHex);
let preTxId = tx.inputs[0].prevTxId.toString('hex');
let preOutputIndex = tx.inputs[0].outputIndex;
let preTxHex = yield this.api.getRawTxData(preTxId);
genesisUtxo.satotxInfo = {
txId: genesisUtxo.txId,
outputIndex: genesisUtxo.outputIndex,
txHex,
preTxId,
preOutputIndex,
preTxHex,
tx,
};
let output = tx.outputs[genesisUtxo.outputIndex];
genesisUtxo.satoshis = output.satoshis;
genesisUtxo.lockingScript = output.script;
genesisContract.setFormatedDataPartFromLockingScript(genesisUtxo.lockingScript);
return {
genesisContract,
genesisTxId,
genesisOutputIndex,
genesisUtxo,
};
});
}
_getMintUtxo(codehash, genesisTxId, genesisOutputIndex, genesisAddress) {
return __awaiter(this, void 0, void 0, function* () {
let unspent;
let firstGenesisTxHex = yield this.api.getRawTxData(genesisTxId);
let firstGenesisTx = new mvc.Transaction(firstGenesisTxHex);
let scriptBuffer = firstGenesisTx.outputs[genesisOutputIndex].script.toBuffer();
let originGenesis = ftProto.getQueryGenesis(scriptBuffer);
let genesisUtxos = yield this.api.getFungibleTokenUnspents(codehash, originGenesis, genesisAddress);
unspent = genesisUtxos.find((v) => v.txId == genesisTxId && v.outputIndex == genesisOutputIndex);
if (!unspent) {
let _dataPartObj = ftProto.parseDataPart(scriptBuffer);
_dataPartObj.sensibleID = {
txid: genesisTxId,
index: genesisOutputIndex,
};
let newScriptBuf = ftProto.updateScript(scriptBuffer, _dataPartObj);
let issueGenesis = ftProto.getQueryGenesis(newScriptBuf);
let issueUtxos = yield this.api.getFungibleTokenUnspents(codehash, issueGenesis, genesisAddress);
if (issueUtxos.length > 0) {
unspent = issueUtxos[0];
}
}
if (unspent) {
return {
txId: unspent.txId,
outputIndex: unspent.outputIndex,
};
}
});
}
_calMintEstimateFee({ genesisUtxoSatoshis, opreturnData, allowIncreaseMints = true, utxoMaxCount = 10, }) {
return __awaiter(this, void 0, void 0, function* () {
let p2pkhInputNum = utxoMaxCount;
let stx = new SizeTransaction_1.SizeTransaction(this.feeb, this.dustCalculator);
stx.addInput(tokenGenesis_1.TokenGenesisFactory.calUnlockingScriptSize(opreturnData), genesisUtxoSatoshis);
for (let i = 0; i < p2pkhInputNum; i++) {
stx.addP2PKHInput();
}
if (allowIncreaseMints) {
stx.addOutput(tokenGenesis_1.TokenGenesisFactory.getLockingScriptSize());
}
stx.addOutput(token_1.TokenFactory.getLockingScriptSize());
if (opreturnData) {
stx.addOpReturnOutput(mvc.Script.buildSafeDataOut(opreturnData).toBuffer().length);
}
stx.addP2PKHOutput();
return stx.getFee();
});
}
merge({ codehash, genesis, ownerWif, utxos, changeAddress, noBroadcast = false, opreturnData, }) {
return __awaiter(this, void 0, void 0, function* () {
$.checkArgument(ownerWif, 'ownerWif is required');
return yield this.transfer({
codehash,
genesis,
senderWif: ownerWif,
utxos,
changeAddress,
isMerge: true,
noBroadcast,
receivers: [],
opreturnData,
});
});
}
_pretreatUtxos(paramUtxos) {
return __awaiter(this, void 0, void 0, function* () {
let utxoPrivateKeys = [];
let utxos = [];
//If utxos are not provided, use purse to fetch utxos
if (!paramUtxos) {
if (!this.purse)
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, 'Utxos or Purse must be provided.');
paramUtxos = yield this.api.getUnspents(this.purse.address.toString());
paramUtxos.forEach((v) => {
utxoPrivateKeys.push(this.purse.privateKey);
});
}
else {
paramUtxos.forEach((v) => {
if (v.wif) {
let privateKey = new mvc.PrivateKey(v.wif);
utxoPrivateKeys.push(privateKey);
v.address = privateKey.toAddress(this.network).toString(); //Compatible with the old version, only wif is provided but no address is provided
}
});
}
paramUtxos.forEach((v) => {
utxos.push({
txId: v.txId,
outputIndex: v.outputIndex,
satoshis: v.satoshis,
address: new mvc.Address(v.address, this.network),
});
});
if (utxos.length == 0)
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, 'Insufficient balance.');
return { utxos, utxoPrivateKeys };
});
}
/**
* Estimate the cost of genesis
* @param opreturnData
* @param utxoMaxCount Maximum number of BSV UTXOs supported
* @returns
*/
getGenesisEstimateFee({ opreturnData, utxoMaxCount = 10, }) {
return __awaiter(this, void 0, void 0, function* () {
const p2pkhInputNum = utxoMaxCount;
const sizeOfTokenGenesis = tokenGenesis_1.TokenGenesisFactory.getLockingScriptSize();
let stx = new SizeTransaction_1.SizeTransaction(this.feeb, this.dustCalculator);
for (let i = 0; i < p2pkhInputNum; i++) {
stx.addP2PKHInput();
}
stx.addOutput(sizeOfTokenGenesis);
if (opreturnData) {
stx.addOpReturnOutput(mvc.Script.buildSafeDataOut(opreturnData).toBuffer().length);
}
stx.addP2PKHOutput();
return stx.getFee();
});
}
_genesis({ tokenName, tokenSymbol, decimalNum, utxos, utxoPrivateKeys, changeAddress, tokenAddress, opreturnData, }) {
return __awaiter(this, void 0, void 0, function* () {
//create genesis contract
let genesisContract = tokenGenesis_1.TokenGenesisFactory.createContract();
genesisContract.setFormatedDataPart({
tokenName,
tokenSymbol,
decimalNum,
tokenAddress,
});
let estimateSatoshis = yield this.getGenesisEstimateFee({
opreturnData,
utxoMaxCount: utxos.length,
});
const balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0);
if (balance < estimateSatoshis) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estimateSatoshis}, but only ${balance}.`);
}
const txComposer = new tx_composer_1.TxComposer();
const p2pkhInputIndexs = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos);
(0, transactionHelpers_1.addContractOutput)({
txComposer,
contract: genesisContract,
dustCalculator: this.dustCalculator,
});
//If there is opReturn, add it to the second output
if (opreturnData) {
txComposer.appendOpReturnOutput(opreturnData);
}
(0, transactionHelpers_1.addChangeOutput)(txComposer, changeAddress, this.feeb);
(0, transactionHelpers_1.unlockP2PKHInputs)(txComposer, p2pkhInputIndexs, utxoPrivateKeys);
(0, transactionHelpers_1.checkFeeRate)(txComposer, this.feeb);
return { txComposer };
});
}
transfer({ codehash, genesis, receivers, senderWif, ftUtxos, ftChangeAddress, utxos, changeAddress, middleChangeAddress, middlePrivateKey, minUtxoSet = true, isMerge, opreturnData, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
checkParamGenesis(genesis);
checkParamCodehash(codehash);
checkParamReceivers(receivers);
let senderPrivateKey;
let senderPublicKey;
if (senderWif) {
senderPrivateKey = new mvc.PrivateKey(senderWif);
}
let utxoInfo = yield this._pretreatUtxos(utxos);
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
if (middleChangeAddress) {
middleChangeAddress = new mvc.Address(middleChangeAddress, this.network);
middlePrivateKey = new mvc.PrivateKey(middlePrivateKey);
}
else {
middleChangeAddress = utxoInfo.utxos[0].address;
middlePrivateKey = utxoInfo.utxoPrivateKeys[0];
}
let ftUtxoInfo = yield this._pretreatFtUtxos(ftUtxos, codehash, genesis, senderPrivateKey, senderPublicKey);
if (ftChangeAddress) {
ftChangeAddress = new mvc.Address(ftChangeAddress, this.network);
}
else {
ftChangeAddress = ftUtxoInfo.ftUtxos[0].tokenAddress;
}
console.log({ ftUtxoInfo });
let { txComposer, transferCheckTxComposer } = yield this._transfer({
codehash,
genesis,
receivers,
ftUtxos: ftUtxoInfo.ftUtxos,
ftPrivateKeys: ftUtxoInfo.ftUtxoPrivateKeys,
ftChangeAddress,
utxos: utxoInfo.utxos,
utxoPrivateKeys: utxoInfo.utxoPrivateKeys,
changeAddress,
opreturnData,
isMerge,
middleChangeAddress,
middlePrivateKey,
minUtxoSet,
});
let routeCheckTxHex = transferCheckTxComposer.getRawHex();
let txHex = txComposer.getRawHex();
const inSats = transferCheckTxComposer
.getTx()
.inputs.reduce((pre, input) => pre + input.output.satoshis, 0);
const outSats = transferCheckTxComposer
.getTx()
.outputs.reduce((pre, output) => pre + output.satoshis, 0);
if (!noBroadcast) {
yield this.api.broadcast(routeCheckTxHex);
yield this.api.broadcast(txHex);
}
return {
tx: txComposer.getTx(),
txHex,
routeCheckTx: transferCheckTxComposer.getTx(),
routeCheckTxHex,
txid: txComposer.getTxId(),
};
});
}
_pretreatFtUtxos(paramFtUtxos, codehash, genesis, senderPrivateKey, senderPublicKey) {
return __awaiter(this, void 0, void 0, function* () {
let ftUtxos = [];
let ftUtxoPrivateKeys = [];
let publicKeys = [];
if (!paramFtUtxos) {
if (senderPrivateKey) {
senderPublicKey = senderPrivateKey.toPublicKey();
}
if (!senderPublicKey)
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, 'ftUtxos or senderPublicKey or senderPrivateKey must be provided.');
paramFtUtxos = yield this.api.getFungibleTokenUnspents(codehash, genesis, senderPublicKey.toAddress(this.network).toString(), 20);
paramFtUtxos.forEach((v) => {
if (senderPrivateKey) {
ftUtxoPrivateKeys.push(senderPrivateKey);
}
publicKeys.push(senderPublicKey);
});
}
else {
paramFtUtxos.forEach((v) => {
if (v.wif) {
let privateKey = new mvc.PrivateKey(v.wif);
ftUtxoPrivateKeys.push(privateKey);
publicKeys.push(privateKey.toPublicKey());
}
});
}
paramFtUtxos.forEach((v, index) => {
ftUtxos.push({
txId: v.txId,
outputIndex: v.outputIndex,
tokenAddress: new mvc.Address(v.tokenAddress, this.network),
tokenAmount: new BN(v.tokenAmount.toString()),
publicKey: publicKeys[index],
});
});
if (ftUtxos.length == 0)
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_FT, 'Insufficient token.');
return { ftUtxos, ftUtxoPrivateKeys };
});
}
_prepareTransferTokens({ codehash, genesis, receivers, ftUtxos, ftChangeAddress, isMerge, minUtxoSet, }) {
return __awaiter(this, void 0, void 0, function* () {
let mergeUtxos = [];
let mergeTokenAmountSum = BN.Zero;
if (isMerge) {
mergeUtxos = ftUtxos.slice(0, 20);
mergeTokenAmountSum = mergeUtxos.reduce((pre, cur) => cur.tokenAmount.add(pre), BN.Zero);
receivers = [
{
address: ftChangeAddress.toString(),
amount: mergeTokenAmountSum.toString(),
},
];
}
let tokenOutputArray = receivers.map((v) => ({
address: new mvc.Address(v.address, this.network),
tokenAmount: new BN(v.amount.toString()),
}));
let outputTokenAmountSum = tokenOutputArray.reduce((pre, cur) => cur.tokenAmount.add(pre), BN.Zero);
let inputTokenAmountSum = BN.Zero;
let _ftUtxos = [];
for (let i = 0; i < ftUtxos.length; i++) {
let ftUtxo = ftUtxos[i];
_ftUtxos.push(ftUtxo);
inputTokenAmountSum = ftUtxo.tokenAmount.add(inputTokenAmountSum);
if (minUtxoSet && inputTokenAmountSum.gte(outputTokenAmountSum)) {
break;
}
}
if (isMerge) {
_ftUtxos = mergeUtxos;
inputTokenAmountSum = mergeTokenAmountSum;
if (mergeTokenAmountSum.eq(BN.Zero)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INNER_ERROR, 'No utxos to merge.');
}
}
//Decide whether to change the token
let changeTokenAmount = inputTokenAmountSum.sub(outputTokenAmountSum);
if (changeTokenAmount.gt(BN.Zero)) {
tokenOutputArray.push({
address: ftChangeAddress,
tokenAmount: changeTokenAmount,
});
}
if (inputTokenAmountSum.lt(outputTokenAmountSum)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_FT, `Insufficient token. Need ${outputTokenAmountSum} But only ${inputTokenAmountSum}`);
}
ftUtxos = _ftUtxos;
yield this.perfectFtUtxosInfo(ftUtxos, codehash, genesis);
let tokenInputArray = ftUtxos;
//Choose a transfer plan
let inputLength = tokenInputArray.length;
let outputLength = tokenOutputArray.length;
let tokenTransferType = tokenTransferCheck_1.TokenTransferCheckFactory.getOptimumType(inputLength, outputLength);
if (tokenTransferType == tokenTransferCheck_1.TOKEN_TRANSFER_TYPE.UNSUPPORT) {
throw new error_1.CodeError(error_1.ErrCode.EC_TOO_MANY_FT_UTXOS, 'Too many token-utxos, should merge them to continue.');
}
// console.log({ tokenTransferType })
// let tokenTransferType = TOKEN_TRANSFER_TYPE.IN_3_OUT_3
return {
tokenInputArray,
tokenOutputArray,
tokenTransferType,
};
});
}
perfectFtUtxosInfo(ftUtxos, codehash, genesis) {
return __awaiter(this, void 0, void 0, function* () {
//Cache txHex to prevent redundant queries
let cachedHexs = {};
//Get txHex
for (let i = 0; i < ftUtxos.length; i++) {
let ftUtxo = ftUtxos[i];
if (!cachedHexs[ftUtxo.txId]) {
cachedHexs[ftUtxo.txId] = {
waitingRes: this.api.getRawTxData(ftUtxo.txId), //async request
};
}
}
for (let id in cachedHexs) {
//Wait for all async requests to complete
if (cachedHexs[id].waitingRes && !cachedHexs[id].hex) {
cachedHexs[id].hex = yield cachedHexs[id].waitingRes;
}
}
ftUtxos.forEach((v) => {
v.satotxInfo = v.satotxInfo || {};
v.satotxInfo.txHex = cachedHexs[v.txId].hex;
v.satotxInfo.txId = v.txId;
v.satotxInfo.outputIndex = v.outputIndex;
});
//Get preTxHex
let curDataPartObj;
for (let i = 0; i < ftUtxos.length; i++) {
let ftUtxo = ftUtxos[i];
const tx = new mvc.Transaction(ftUtxo.satotxInfo.txHex);
if (!curDataPartObj) {
let tokenScript = tx.outputs[ftUtxo.outputIndex].script;
curDataPartObj = ftProto.parseDataPart(tokenScript.toBuffer());
}
//Find a valid preTx
let prevTokenInputIndex = 0;
let input = tx.inputs.find((input, inputIndex) => {
let script = new mvc.Script(input.script);
if (script.chunks.length > 0) {
const lockingScriptBuf = TokenUtil.getLockingScriptFromPreimage(script.chunks[0].buf);
if (lockingScriptBuf) {
if (ftProto.getQueryGenesis(lockingScriptBuf) == genesis) {
prevTokenInputIndex = inputIndex;
return true;
}
let dataPartObj = ftProto.parseDataPart(lockingScriptBuf);
dataPartObj.sensibleID = curDataPartObj.sensibleID;
const newScriptBuf = ftProto.updateScript(lockingScriptBuf, dataPartObj);
let genesisHash = (0, scryptlib_1.toHex)(mvc.crypto.Hash.sha256ripemd160(newScriptBuf));
if (genesisHash == curDataPartObj.genesisHash) {
prevTokenInputIndex = inputIndex;
return true;
}
}
}
});
if (!input) {
throw new error_1.CodeError(error_1.ErrCode.EC_INNER_ERROR, 'There is no valid preTx of the ftUtxo. ');
}
let preTxId = input.prevTxId.toString('hex');
let preOutputIndex = input.outputIndex;
ftUtxo.satotxInfo.preTxId = preTxId;
ftUtxo.satotxInfo.preOutputIndex = preOutputIndex;
ftUtxo.satotxInfo.txInputsCount = tx.inputs.length;
ftUtxo.satoshis = tx.outputs[ftUtxo.outputIndex].satoshis;
ftUtxo.lockingScript = tx.outputs[ftUtxo.outputIndex].script;
// 新增字段 prevTokenInputIndex, prevTokenOutputIndex
ftUtxo.prevTokenOutputIndex = input.outputIndex;
ftUtxo.prevTokenInputIndex = prevTokenInputIndex;
if (!cachedHexs[preTxId]) {
cachedHexs[preTxId] = {
waitingRes: this.api.getRawTxData(preTxId),
};
}
}
for (let id in cachedHexs) {
//Wait for all async requests to complete
if (cachedHexs[id].waitingRes && !cachedHexs[id].hex) {
cachedHexs[id].hex = yield cachedHexs[id].waitingRes;
}
}
ftUtxos.forEach((v) => {
v.satotxInfo.preTxHex = cachedHexs[v.satotxInfo.preTxId].hex;
const preTx = new mvc.Transaction(v.satotxInfo.preTxHex);
let dataPartObj = ftProto.parseDataPart(preTx.outputs[v.satotxInfo.preOutputIndex].script.toBuffer());
v.preTokenAmount = new BN(dataPartObj.tokenAmount.toString());
if (dataPartObj.tokenAddress == '0000000000000000000000000000000000000000') {
v.preTokenAddress = this.zeroAddress;
}
else {
v.preTokenAddress = mvc.Address.fromPublicKeyHash(Buffer.from(dataPartObj.tokenAddress, 'hex'), this.network);
}
v.preLockingScript = preTx.outputs[v.satotxInfo.preOutputIndex].script;
// 新增字段 prevTokenTx,
v.prevTokenTx = preTx;
});
// ftUtxos.forEach((v) => {
// v.preTokenAmount = new BN(v.preTokenAmount.toString())
// })
return ftUtxos;
});
}
_transfer({ codehash, genesis, receivers, ftUtxos, ftPrivateKeys, ftChangeAddress, utxos, utxoPrivateKeys, changeAddress, middlePrivateKey, middleChangeAddress, isMerge, opreturnData, minUtxoSet, }) {
return __awaiter(this, void 0, void 0, function* () {
if (utxos.length > 3) {
throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, 'Bsv utxos should be no more than 3 in the transfer operation, please merge it first ');
}
if (!middleChangeAddress) {
middleChangeAddress = utxos[0].address;
middlePrivateKey = utxoPrivateKeys[0];
}
let { tokenInputArray, tokenOutputArray, tokenTransferType } = yield this._prepareTransferTokens({
codehash,
genesis,
receivers,
ftUtxos,
ftChangeAddress,
isMerge,
minUtxoSet,
});
let estimateSatoshis = this._calTransferEstimateFee({
p2pkhInputNum: utxos.length,
tokenInputArray,
tokenOutputArray,
tokenTransferType,
opreturnData,
});
const balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0);
if (balance < estimateSatoshis) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estimateSatoshis}, but only ${balance}.`);
}
ftUtxos = tokenInputArray;
const defaultFtUtxo = tokenInputArray[0];
const ftUtxoTx = new mvc.Transaction(defaultFtUtxo.satotxInfo.txHex);
const tokenLockingScript = ftUtxoTx.outputs[defaultFtUtxo.outputIndex].script;
//create routeCheck contract
let tokenTransferCheckContract = tokenTransferCheck_1.TokenTransferCheckFactory.createContract(tokenTransferType);
tokenTransferCheckContract.setFormatedDataPart({
nSenders: tokenInputArray.length,
receiverTokenAmountArray: tokenOutputArray.map((v) => v.tokenAmount),
receiverArray: tokenOutputArray.map((v) => v.address),
nReceivers: tokenOutputArray.length,
tokenCodeHash: (0, scryptlib_1.toHex)(ftProto.getContractCodeHash(tokenLockingScript.toBuffer())),
tokenID: (0, scryptlib_1.toHex)(ftProto.getTokenID(tokenLockingScript.toBuffer())),
});
const transferCheckTxComposer = new tx_composer_1.TxComposer();
//tx addInput utxo
const transferCheck_p2pkhInputIndexs = utxos.map((utxo) => {
const inputIndex = transferCheckTxComposer.appendP2PKHInput(utxo);
transferCheckTxComposer.addSigHashInfo({
inputIndex,
address: utxo.address.toString(),
sighashType: exports.sighashType,
contractType: utils_1.CONTRACT_TYPE.P2PKH,
});
return inputIndex;
});
const transferCheckOutputIndex = transferCheckTxComposer.appendOutput({
lockingScript: tokenTransferCheckContract.lockingScript,
satoshis: this.getDustThreshold(tokenTransferCheckContract.lockingScript.toBuffer().length),
});
let changeOutputIndex = transferCheckTxComposer.appendChangeOutput(middleChangeAddress, this.feeb);
let unsignSigPlaceHolderSize = 0;
if (utxoPrivateKeys && utxoPrivateKeys.length > 0) {
transferCheck_p2pkhInputIndexs.forEach((inputIndex) => {
let privateKey = utxoPrivateKeys.splice(0, 1)[0];
transferCheckTxComposer.unlockP2PKHInput(privateKey, inputIndex);
});
}
else {
//To supplement the size calculation when unsigned
transferCheck_p2pkhInputIndexs.forEach((v) => {
unsignSigPlaceHolderSize += utils_1.P2PKH_UNLOCK_SIZE;
});
//Each ftUtxo need to unlock with the size
unsignSigPlaceHolderSize = unsignSigPlaceHolderSize * ftUtxos.length;
}
utxos = [
{
txId: transferCheckTxComposer.getTxId(),
satoshis: transferCheckTxComposer.getOutput(changeOutputIndex).satoshis,
outputIndex: changeOutputIndex,
address: middleChangeAddress,
},
];
utxoPrivateKeys = utxos.map((v) => middlePrivateKey).filter((v) => v);
let transferCheckUtxo = {
txId: transferCheckTxComposer.getTxId(),
outputIndex: transferCheckOutputIndex,
satoshis: transferCheckTxComposer.getOutput(transferCheckOutputIndex).satoshis,
lockingScript: transferCheckTxComposer.getOutput(transferCheckOutputIndex).script,
};
let transferCheckTx = transferCheckTxComposer.getTx();
const txComposer = new tx_composer_1.TxComposer();
let prevouts = new Prevouts_1.Prevouts();
let inputTokenScript;
let inputTokenAmountArray = Buffer.alloc(0);
let inputTokenAddressArray = Buffer.alloc(0);
const ftUtxoInputIndexs = ftUtxos.map((ftUtxo) => {
const inputIndex = txComposer.appendInput(ftUtxo);
prevouts.addVout(ftUtxo.txId, ftUtxo.outputIndex);
txComposer.addSigHashInfo({
inputIndex,
address: ftUtxo.tokenAddress.toString(),
sighashType: exports.sighashType,
contractType: utils_1.CONTRACT_TYPE.BCP02_TOKEN,
});
inputTokenScript = ftUtxo.lockingScript;
inputTokenAddressArray = Buffer.concat([
inputTokenAddressArray,
ftUtxo.tokenAddress.hashBuffer,
]);
inputTokenAmountArray = Buffer.concat([
inputTokenAmountArray,
ftUtxo.tokenAmount.toBuffer({
endian: 'little',
size: 8,
}),
]);
return inputIndex;
});
//tx addInput utxo
const p2pkhInputIndexs = utxos.map((utxo) => {
const inputIndex = txComposer.appendP2PKHInput(utxo);
prevouts.addVout(utxo.txId, utxo.outputIndex);
txComposer.addSigHashInfo({
inputIndex,
address: utxo.address.toString(),
sighashType: exports.sighashType,
contractType: utils_1.CONTRACT_TYPE.P2PKH,
});
return inputIndex;
});
//添加transferCheck为最后一个输入
const transferCheckInputIndex = txComposer.appendInput(transferCheckUtxo);
prevouts.addVout(transferCheckUtxo.txId, transferCheckUtxo.outputIndex);
let recervierArray = Buffer.alloc(0);
let receiverTokenAmountArray = Buffer.alloc(0);
let outputSatoshiArray = Buffer.alloc(0);
const tokenOutputLen = tokenOutputArray.length;
for (let i = 0; i < tokenOutputLen; i++) {
const tokenOutput = tokenOutputArray[i];
const address = tokenOutput.address;
const outputTokenAmount = tokenOutput.tokenAmount;
const lockingScriptBuf = ftProto.getNewTokenScript(inputTokenScript.toBuffer(), address.hashBuffer, outputTokenAmount);
let outputIndex = txComposer.appendOutput({
lockingScript: mvc.Script.fromBuffer(lockingScriptBuf),
satoshis: this.getDustThreshold(lockingScriptBuf.length),
});
recervierArray = Buffer.concat([recervierArray, address.hashBuffer]);
const tokenBuf = outputTokenAmount.toBuffer({
endian: 'little',
size: 8,
});
receiverTokenAmountArray = Buffer.concat([receiverTokenAmountArray, tokenBuf]);
const satoshiBuf = BN.fromNumber(txComposer.getOutput(outputIndex).satoshis).toBuffer({
endian: 'little',
size: 8,
});
outputSatoshiArray = Buffer.concat([outputSatoshiArray, satoshiBuf]);
}
//tx addOutput OpReturn
let opreturnScriptHex = '';
if (opreturnData) {
const opreturnOutputIndex = txComposer.appendOpReturnOutput(opreturnData);
opreturnScriptHex = txComposer.getOutput(opreturnOutputIndex).script.toHex();
}
//The first round of calculations get the exact size of the final transaction, and then change again
//Due to the change, the script needs to be unlocked again in the second round
//let the fee to be exact in the second round
for (let c = 0; c < 2; c++) {
txComposer.clearChangeOutput();
const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb, unsignSigPlaceHolderSize);
let