meta-contract
Version:
Meta Contract SDK
1,013 lines • 92.4 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 tokenUtil_1 = 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 tokenUnlockContractCheck_1 = require("../mcp02/contract-factory/tokenUnlockContractCheck");
const buffer_1 = require("buffer");
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' ||
codehash === 'e205939ad9956673ce7da9fbd40514b30f66dc35' ||
codehash === 'c9cc7bbd1010b44873959a8b1a2bcedeb62302b7', `a valid codehash should be ${contractUtil_1.ContractUtil.tokenCodeHash}, but the provided is ${codehash} `);
}
function determineCodehashVersion(codehash) {
if (codehash == contractUtil_1.ContractUtil.tokenCodeHash) {
return 2;
}
return 1;
}
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_1.Buffer.from(sensibleID, 'hex');
let genesisTxId = sensibleIDBuf.slice(0, 32).reverse().toString('hex');
let genesisOutputIndex = sensibleIDBuf.readUIntLE(32, 4);
return {
genesisTxId,
genesisOutputIndex,
};
}
class FtManager {
get api() {
return this._api;
}
get sensibleApi() {
return this._api;
}
constructor({ network = __1.API_NET.MAIN, apiTarget = __1.API_TARGET.CYBER3, 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(constants_1.BURN_ADDRESS, network);
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 = debug;
}
/**
* 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({ version = 2, tokenName, tokenSymbol, decimalNum, utxos: utxosInput, changeAddress, opreturnData, genesisWif, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
// validate params
$.checkArgument(_.isString(tokenName) && buffer_1.Buffer.from(tokenName).length <= 40, `tokenName should be a string and not be larger than 40 bytes`);
$.checkArgument(_.isString(tokenSymbol) && buffer_1.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');
$.checkArgument(_.isNumber(version) && version >= 1 && version <= 2, 'version should be a number and must be between 1 and 2');
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)({
version,
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({ version = 2, sensibleId, genesisWif, receiverAddress, tokenAmount, allowIncreaseMints = true, utxos, changeAddress, opreturnData, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
$.checkArgument(sensibleId, 'sensibleId is required');
$.checkArgument(genesisWif, 'genesisWif is required');
$.checkArgument(receiverAddress, 'receiverAddress is required');
$.checkArgument(tokenAmount, 'tokenAmount is required');
const utxoInfo = yield this._pretreatUtxos(utxos);
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({
version,
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({ version, 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_MVC, `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, version);
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,
});
});
}
totalMerge({ codehash, genesis, ownerWif, changeAddress, opreturnData, }) {
return __awaiter(this, void 0, void 0, function* () {
$.checkArgument(ownerWif, 'ownerWif is required');
const ownerPublicKey = mvc.PrivateKey.fromWIF(ownerWif).toPublicKey();
const txids = [];
while (true) {
const ftUtxos = yield this.api.getFungibleTokenUnspents(codehash, genesis, ownerPublicKey.toAddress(this.network).toString(), 20);
if (ftUtxos.length <= 1) {
break;
}
const paramFtUtxos = ftUtxos.map((v) => {
return {
txId: v.txId,
outputIndex: v.outputIndex,
tokenAddress: v.tokenAddress,
tokenAmount: v.tokenAmount,
wif: ownerWif,
};
});
const { txid } = yield this.transfer({
codehash,
genesis,
senderWif: ownerWif,
ftUtxos: paramFtUtxos,
changeAddress,
isMerge: true,
receivers: [],
opreturnData,
});
txids.push(txid);
// sleep 1s
yield new Promise((resolve) => setTimeout(resolve, 1000));
}
return { txids };
});
}
_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_MVC, 'Insufficient balance.');
return { utxos, utxoPrivateKeys };
});
}
/**
* Estimate the cost of genesis
* @param opreturnData
* @param utxoMaxCount Maximum number of MVC 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_MVC, `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);
const version = determineCodehashVersion(codehash);
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;
}
let { txComposer, transferCheckTxComposer } = yield this._transfer({
version,
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();
if (!noBroadcast) {
yield this.api.broadcast(routeCheckTxHex);
yield this.api.broadcast(txHex);
}
return {
tx: txComposer.getTx(),
txHex,
routeCheckTx: transferCheckTxComposer.getTx(),
routeCheckTxHex,
txid: txComposer.getTxId(),
};
});
}
/**
* burn token
* @param codehash token codehash
* @param genesis token genesis
* @param ftUtxos ft utxos to burn, must be sent to zero address
* @param utxos fee provider utxos
* @param utxoPrivateKey fee provider utxo private key
* @param changeAddress satoshi change address
* @param opreturnData opreturn data
* @param noBroadcast if true, will not broadcast tx
*/
burn({ codehash, genesis, ftUtxos, utxos, utxoPrivateKey, changeAddress, opreturnData, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
checkParamGenesis(genesis);
checkParamCodehash(codehash);
const version = determineCodehashVersion(codehash);
let utxoInfo = yield this._pretreatUtxos(utxos);
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
let ftUtxoInfo = yield this._pretreatFtUtxos(ftUtxos, codehash, genesis);
let { unlockCheckTxComposer, txComposer } = yield this._burn({
version,
genesis,
ftUtxos: ftUtxoInfo.ftUtxos,
utxos: utxoInfo.utxos,
utxoPrivateKey: utxoInfo.utxoPrivateKeys[0],
changeAddress,
opreturnData,
});
let routeCheckTxHex = unlockCheckTxComposer.getRawHex();
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.api.broadcast(routeCheckTxHex);
yield this.api.broadcast(txHex);
}
return {
tx: txComposer.getTx(),
txHex,
routeCheckTx: unlockCheckTxComposer.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 };
});
}
/**
* prepare transfer tokens, decide which transfer pattern to use, preprocess ft utxos(fetch previous transactions for tx building)
* @param codehash codehash of token
* @param genesis genesis of token
* @param receivers token receivers, will be ignored if isMerge is true
* @param ftUtxos input ft utxos
* @param ftChangeAddress change address of ft
* @param isMerge merge utxos, if true, all the token will be merged into one utxo and send to the change address
* @param minUtxoSet if true, will use minimum utxo set as possible
* @private
*/
_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, 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.');
}
return {
tokenInputArray,
tokenOutputArray,
tokenTransferType,
};
});
}
/**
* prepare burn tokens,preprocess ft utxos(fetch previous transactions for tx building)
* @param codehash codehash of token
* @param genesis genesis of token
* @param ftUtxos input ft utxos
* @private
*/
_prepareBurnTokens({ genesis, ftUtxos }) {
return __awaiter(this, void 0, void 0, function* () {
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);
}
ftUtxos = _ftUtxos;
yield this.perfectFtUtxosInfo(ftUtxos, genesis);
let tokenInputArray = ftUtxos;
// burn tx have only one output to receive satoshi
let tokenUnlockType = tokenUnlockContractCheck_1.TokenUnlockContractCheckFactory.getOptimumType(ftUtxos.length, 1);
if (tokenUnlockType == tokenUnlockContractCheck_1.TOKEN_UNLOCK_TYPE.UNSUPPORT) {
throw new error_1.CodeError(error_1.ErrCode.EC_TOO_MANY_FT_UTXOS, 'Too many token-utxos, should merge them to continue.');
}
return {
tokenInputArray,
tokenUnlockType,
};
});
}
/**
* Fetch previous transactions for each ft utxo
* @param ftUtxos ft utxos
* @param genesis genesis of token
* @private
*/
perfectFtUtxosInfo(ftUtxos, 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_1.Buffer.from(dataPartObj.tokenAddress, 'hex'), this.network);
}
v.preLockingScript = preTx.outputs[v.satotxInfo.preOutputIndex].script;
// 新增字段 prevTokenTx,
v.prevTokenTx = preTx;
});
return ftUtxos;
});
}
/**
* composite a token transfer transaction and amount check transaction
* @param codehash codehash of the token
* @param genesis genesis of the token
* @param receivers token receivers
* @param ftUtxos input ftUtxos
* @param ftPrivateKeys private keys of ftUtxos
* @param ftChangeAddress change address of ftUtxos
* @param utxos utxos for paying fee
* @param utxoPrivateKeys private keys of utxos(fee paying)
* @param changeAddress change address of utxos(fee paying)
* @param middlePrivateKey
* @param middleChangeAddress
* @param isMerge whether to merge the token utxos
* @param opreturnData opreturn data to be added to the transaction
* @param minUtxoSet
* @private
*/
_transfer({ version, codehash, genesis, receivers, ftUtxos, ftPrivateKeys, ftChangeAddress, utxos, utxoPrivateKeys, changeAddress, middlePrivateKey, middleChangeAddress, isMerge, opreturnData, minUtxoSet, }) {
return __awaiter(this, void 0, void 0, function* () {
// limit the number of fee paying utxos
if (utxos.length > 3) {
throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, 'Mvc utxos should be no more than 3 in the transfer operation, please merge it first ');
}
if (!middleChangeAddress) {
middleChangeAddress = utxos[0].address;
middlePrivateKey = utxoPrivateKeys[0];
}
// preprocess the ftUtxos, fetch previous tx hex and parse the token amount. decide the token transfer type.
let { tokenInputArray, tokenOutputArray, tokenTransferType } = yield this._prepareTransferTokens({
codehash,
genesis,
receivers,
ftUtxos,
ftChangeAddress,
isMerge,
minUtxoSet,
});
// calculate the fee
let estimateSatoshis = this._calTransferEstimateFee({
p2pkhInputNum: utxos.length,
tokenInputArray,
tokenOutputArray,
tokenTransferType,
opreturnData,
});
// if fee is not enough, throw error
const balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0);
if (balance < estimateSatoshis) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_MVC, `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 transferCheck 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();
// add utxo to provide fee for transfer check transaction
const transferCheck_p2pkhInputIndexs = utxos.map((utxo) => {
const inputIndex = transferCheckTxComposer.appendP2PKHInput(utxo);
transferChe