meta-contract-debug
Version:
Meta Contract SDK
986 lines (985 loc) • 99.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.NftManager = void 0;
const DustCalculator_1 = require("../common/DustCalculator");
const tx_composer_1 = require("../tx-composer");
const mvc = require("../mvc");
const __1 = require("..");
const nftGenesis_1 = require("./contract-factory/nftGenesis");
const nftSell_1 = require("./contract-factory/nftSell");
const transactionHelpers_1 = require("../helpers/transactionHelpers");
const nft_1 = require("./contract-factory/nft");
const scryptlib_1 = require("../scryptlib");
const mvc_1 = require("../mvc");
const TokenUtil = require("../common/tokenUtil");
const nftProto = require("./contract-proto/nft.proto");
const nftSellProto = require("./contract-proto/nftSell.proto");
const Signature = mvc.crypto.Signature;
const contractUtil_1 = require("./contractUtil");
const utils_1 = require("../common/utils");
const Prevouts_1 = require("../common/Prevouts");
const error_1 = require("../common/error");
const SizeTransaction_1 = require("../common/SizeTransaction");
const protoheader_1 = require("../common/protoheader");
const contractHelpers_1 = require("../helpers/contractHelpers");
const proofHelpers_1 = require("../helpers/proofHelpers");
const constants_1 = require("../mcp02/constants");
const nftUnlockContractCheck_1 = require("./contract-factory/nftUnlockContractCheck");
const dummy_1 = require("../common/dummy");
contractUtil_1.ContractUtil.init();
const jsonDescr = require('./contract-desc/txUtil_desc.json');
const { TxInputProof, TxOutputProof } = (0, scryptlib_1.buildTypeClasses)(jsonDescr);
class NftManager {
constructor({ purse, network = __1.API_NET.MAIN, apiTarget = __1.API_TARGET.MVC, apiHost, feeb = constants_1.FEEB, debug = false, }) {
this.dustCalculator = new DustCalculator_1.DustCalculator(mvc_1.Transaction.DUST_AMOUNT, null);
this.network = network;
this._api = new __1.Api(network, apiTarget, apiHost);
this.unlockContractCodeHashArray = contractUtil_1.ContractUtil.unlockContractCodeHashArray;
if (feeb)
this.feeb = feeb;
this.debug = debug;
if (purse) {
const privateKey = mvc.PrivateKey.fromWIF(purse);
const address = privateKey.toAddress(this.network);
this.purse = {
privateKey,
address,
};
}
}
get api() {
return this._api;
}
get sensibleApi() {
return this._api;
}
/**
* Estimate the cost of genesis
* The minimum cost required in the case of 10 utxo inputs
* @param opreturnData
* @param utxoMaxCount Maximum number of BSV UTXOs supported
* @returns
*/
getGenesisEstimateFee({ opreturnData, utxoMaxCount = 10, }) {
return __awaiter(this, void 0, void 0, function* () {
let p2pkhInputNum = utxoMaxCount;
let stx = new SizeTransaction_1.SizeTransaction(this.feeb, this.dustCalculator);
for (let i = 0; i < p2pkhInputNum; i++) {
stx.addP2PKHInput();
}
stx.addOutput(nftGenesis_1.NftGenesisFactory.getLockingScriptSize());
if (opreturnData) {
stx.addOpReturnOutput(mvc.Script.buildSafeDataOut(opreturnData).toBuffer().length);
}
stx.addP2PKHOutput();
return stx.getFee();
});
}
getIssueEstimateFee({ sensibleId, opreturnData, utxoMaxCount = 10, }) {
return __awaiter(this, void 0, void 0, function* () {
const { genesisUtxo } = (yield (0, transactionHelpers_1.getLatestGenesisInfo)({
sensibleId,
api: this.api,
address: this.purse.address,
type: 'nft',
}));
return yield this._calIssueEstimateFee({
genesisUtxoSatoshis: genesisUtxo.satoshis,
opreturnData,
utxoMaxCount,
});
});
}
getTransferEstimateFee({ tokenIndex, codehash, genesis, opreturnData, utxoMaxCount = 10, }) {
return __awaiter(this, void 0, void 0, function* () {
let { nftUtxo } = yield (0, transactionHelpers_1.getNftInfo)({
tokenIndex,
codehash,
genesis,
api: this.api,
network: this.network,
});
nftUtxo = yield this.pretreatNftUtxo(nftUtxo, codehash, genesis);
const genesisScript = new scryptlib_1.Bytes(nftUtxo.preLockingScript.toHex());
return yield this._calTransferEstimateFee({
nftUtxoSatoshis: nftUtxo.satoshis,
genesisScript,
opreturnData,
utxoMaxCount,
});
});
}
genesis({ genesisWif, totalSupply, opreturnData, utxos: utxosInput, changeAddress, noBroadcast = false, calcFee = false, }) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
if (calcFee) {
return {
fee: yield this.getGenesisEstimateFee({ opreturnData }),
feeb: this.feeb,
};
}
const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput);
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxos[0].address;
}
const { txComposer, genesisContract } = yield this.createGenesisTx({
totalSupply,
utxos,
utxoPrivateKeys,
opreturnData,
changeAddress,
});
if (calcFee) {
// const unlockSize =
// txComposer.tx.inputs.filter((v) => v.output.script.isPublicKeyHashOut()).length *
// P2PKH_UNLOCK_SIZE
// let fee = Math.ceil(
// (txComposer.tx.toBuffer().length + unlockSize + mvc.Transaction.CHANGE_OUTPUT_MAX_SIZE) *
// this.feeb
// )
let fee = Math.ceil(txComposer.tx._estimateSize() * this.feeb);
return { fee, feeb: this.feeb };
}
let txHex = txComposer.getRawHex();
let txid;
if (!noBroadcast) {
txid = yield this.api.broadcast(txHex);
}
let { codehash, genesis, sensibleId } = (0, contractHelpers_1.getGenesisIdentifiers)({
genesisTx: txComposer.getTx(),
purse: this.purse,
unlockContractCodeHashArray: this.unlockContractCodeHashArray,
type: 'nft',
});
const runtime = Date.now() - startTime;
return {
codehash,
genesis,
sensibleId,
tx: txComposer.tx,
txid: txComposer.tx.id,
txHex,
genesisContract,
broadcastStatus: noBroadcast ? 'pending' : txid ? 'success' : 'fail',
runtime,
};
});
}
createGenesisTx({ totalSupply, utxos, utxoPrivateKeys, opreturnData, changeAddress, }) {
return __awaiter(this, void 0, void 0, function* () {
const txComposer = new tx_composer_1.TxComposer();
// 构建合约
const genesisContract = (0, contractHelpers_1.createNftGenesisContract)({ totalSupply, address: this.purse.address });
// 添加付钱输入、添加创世输出、添加找零输出、解锁输入
const p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos);
(0, transactionHelpers_1.addContractOutput)({
txComposer,
contract: genesisContract,
dustCalculator: this.dustCalculator,
});
// 添加opreturn输出
if (opreturnData) {
(0, transactionHelpers_1.addOpreturnOutput)(txComposer, opreturnData);
}
(0, transactionHelpers_1.addChangeOutput)(txComposer, changeAddress, this.feeb);
(0, transactionHelpers_1.unlockP2PKHInputs)(txComposer, p2pkhInputIndexes, utxoPrivateKeys);
// 检查最终费率
(0, transactionHelpers_1.checkFeeRate)(txComposer, this.feeb);
return { txComposer, genesisContract };
});
}
issue(options) {
return __awaiter(this, void 0, void 0, function* () {
return this.mint(options);
});
}
mint({ sensibleId, metaTxId, metaOutputIndex, opreturnData, utxos: utxosInput, receiverAddress, changeAddress, noBroadcast = false, calcFee = false, }) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
if (calcFee) {
return {
fee: yield this.getIssueEstimateFee({ sensibleId, opreturnData }),
feeb: this.feeb,
};
}
const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput);
const genesisPrivateKey = this.purse.privateKey;
const genesisPublicKey = genesisPrivateKey.toPublicKey();
if (receiverAddress) {
receiverAddress = new mvc.Address(receiverAddress, this.network);
}
else {
receiverAddress = this.purse.address;
}
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxos[0].address;
}
if (calcFee) {
return yield this.createMintTx({
utxos,
utxoPrivateKeys,
sensibleId,
metaTxId,
metaOutputIndex,
opreturnData,
receiverAddress,
changeAddress,
calcFee,
});
}
const { txComposer, tokenIndex } = yield this.createMintTx({
utxos,
utxoPrivateKeys,
sensibleId,
metaTxId,
metaOutputIndex,
opreturnData,
receiverAddress,
changeAddress,
});
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
const res = yield this.api.broadcast(txHex);
}
const runtime = Date.now() - startTime;
return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx(), tokenIndex, runtime };
});
}
transfer({ genesis, codehash, tokenIndex, senderWif, receiverAddress, opreturnData, utxos: utxosInput, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput);
receiverAddress = new mvc.Address(receiverAddress, this.network);
const { txComposer } = yield this.createTransferTx({
utxos,
utxoPrivateKeys,
genesis,
codehash,
tokenIndex,
receiverAddress,
opreturnData,
});
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.api.broadcast(txHex);
}
const runtime = Date.now() - startTime;
return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx(), runtime };
});
}
sell({ genesis, codehash, tokenIndex, sellerWif, price, changeAddress, opreturnData, utxos: utxosInput, noBroadcast = false, middleChangeAddress, middleWif, }) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
// checkParamGenesis(genesis)
// checkParamCodehash(codehash)
// 检查售价:不能低于22000聪
if (price < 22000) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, 'Selling Price must be greater than or equals to 22000 satoshis. 销售价格最低为22000聪。');
}
// 准备钱💰;utxo不能超过3个
const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput);
if (utxos.length > 3) {
throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, '销售合约使用的utxo数量应当少于等于3个,请先归集utxo。MVC utxos should be no more than 3 in this operation, please merge it first.');
}
// 检查此NFT是否属于卖家
const sellerPrivateKey = new mvc.PrivateKey(sellerWif);
const sellerPublicKey = sellerPrivateKey.publicKey;
let { nftUtxo } = yield (0, transactionHelpers_1.getNftInfo)({
tokenIndex,
codehash,
genesis,
api: this.api,
network: this.network,
});
if (nftUtxo.nftAddress.toString() != sellerPublicKey.toAddress(this.network).toString()) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, 'nft销售者应当为nft持有者!nft seller should be the nft owner!');
}
// 准备找零地址
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxos[0].address;
}
// 准备中间找零地址
let middlePrivateKey;
if (middleChangeAddress) {
middleChangeAddress = new mvc.Address(middleChangeAddress, this.network);
middlePrivateKey = new mvc.PrivateKey(middleWif);
}
else {
middleChangeAddress = utxos[0].address;
middlePrivateKey = utxoPrivateKeys[0];
}
const { sellTxComposer, txComposer } = yield this.createSellTx({
utxos,
utxoPrivateKeys,
genesis,
codehash,
tokenIndex,
nftUtxo,
price,
opreturnData,
changeAddress,
middlePrivateKey,
middleChangeAddress,
});
let nftSellTxHex = sellTxComposer.getRawHex();
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.api.broadcast(nftSellTxHex);
yield this.api.broadcast(txHex);
}
const runtime = Date.now() - startTime;
return {
tx: txComposer.tx,
txHex,
txid: txComposer.tx.id,
sellTxId: sellTxComposer.getTxId(),
sellTx: sellTxComposer.getTx(),
sellTxHex: nftSellTxHex,
runtime,
};
});
}
cancelSell({ genesis, codehash, tokenIndex, sellerWif, sellUtxo, opreturnData, utxos: utxosInput, changeAddress, noBroadcast = false, middleChangeAddress, middlePrivateKey, }) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
// checkParamGenesis(genesis)
// checkParamCodehash(codehash)
const sellerPrivateKey = new mvc.PrivateKey(sellerWif);
// 准备钱💰;utxo不能超过3个
const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput);
if (utxos.length > 3) {
throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, '下架合约使用的utxo数量应当少于等于3个,请先归集utxo。MVC utxos should be no more than 3 in this operation, please merge it first.');
}
// 准备找零地址
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxos[0].address;
}
// 准备中间找零地址
if (middleChangeAddress) {
middleChangeAddress = new mvc.Address(middleChangeAddress, this.network);
middlePrivateKey = new mvc.PrivateKey(middlePrivateKey);
}
else {
middleChangeAddress = utxos[0].address;
middlePrivateKey = utxoPrivateKeys[0];
}
const { unlockCheckTxComposer, txComposer } = yield this.createCancelSellTx({
utxos,
utxoPrivateKeys,
genesis,
codehash,
tokenIndex,
sellUtxo,
sellerPrivateKey,
opreturnData,
changeAddress,
middlePrivateKey,
middleChangeAddress,
});
let unlockCheckTxHex = unlockCheckTxComposer.getRawHex();
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.api.broadcast(unlockCheckTxHex);
yield this.api.broadcast(txHex);
}
const runtime = Date.now() - startTime;
return {
tx: txComposer.tx,
txHex,
txid: txComposer.tx.id,
unlockCheckTxId: unlockCheckTxComposer.getTxId(),
unlockCheckTx: unlockCheckTxComposer.getTx(),
unlockCheckTxHex: unlockCheckTxHex,
runtime,
};
});
}
createCancelSellTx({ utxos, utxoPrivateKeys, genesis, codehash, tokenIndex, sellUtxo, sellerPrivateKey, opreturnData, changeAddress, middlePrivateKey, middleChangeAddress, }) {
return __awaiter(this, void 0, void 0, function* () {
// 第一步:找回并准备NFT Utxo
// 1.1 找回nft Utxo
let { nftUtxo } = yield (0, transactionHelpers_1.getNftInfo)({
tokenIndex,
codehash,
genesis,
api: this.api,
network: this.network,
});
// 1.2 验证nft Utxo
nftUtxo = yield this.pretreatNftUtxo(nftUtxo, codehash, genesis);
// 第二步:找到并重建销售utxo
// 2.1 查找销售utxo
if (!sellUtxo) {
sellUtxo = yield this.api.getNftSellUtxo(codehash, genesis, tokenIndex);
}
if (!sellUtxo) {
throw new error_1.CodeError(error_1.ErrCode.EC_NFT_NOT_ON_SELL, 'The NFT is not for sale because the corresponding SellUtxo cannot be found.');
}
// 2.2 重建销售utxo
let nftAddress = sellerPrivateKey.toAddress(this.network);
let nftSellTxHex = yield this.api.getRawTxData(sellUtxo.txId);
let nftSellTx = new mvc.Transaction(nftSellTxHex);
let nftSellUtxo = {
txId: sellUtxo.txId,
outputIndex: sellUtxo.outputIndex,
satoshis: nftSellTx.outputs[sellUtxo.outputIndex].satoshis,
lockingScript: nftSellTx.outputs[sellUtxo.outputIndex].script,
};
// 第三步:确保余额充足(需要构造三个交易)
// let genesisScript = nftUtxo.preNftAddress.hashBuffer.equals(Buffer.alloc(20, 0))
// ? new Bytes(nftUtxo.preLockingScript.toHex())
// : new Bytes('')
let genesisScript = new scryptlib_1.Bytes(nftUtxo.preLockingScript.toHex());
let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0);
let estCancelSellFee = yield this._calCancelSellEstimateFee({
codehash,
nftUtxoSatoshis: nftUtxo.satoshis,
nftSellUtxo,
genesisScript,
utxoMaxCount: utxos.length,
opreturnData,
});
if (balance < estCancelSellFee) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estCancelSellFee}, but only ${balance}.`);
}
// 第四步:构建解锁交易
// 4.1 准备nft解锁数据
let nftInput = nftUtxo;
let nftID = nftProto.getNftID(nftInput.lockingScript.toBuffer());
let unlockContract = nftUnlockContractCheck_1.NftUnlockContractCheckFactory.createContract(nftUnlockContractCheck_1.NFT_UNLOCK_CONTRACT_TYPE.OUT_6);
unlockContract.setFormatedDataPart({
nftCodeHash: Buffer.from(codehash, 'hex'),
nftID,
});
// 解锁合约交易构建器
const unlockCheckTxComposer = new tx_composer_1.TxComposer();
// 4.2 往解锁合约交易中塞钱💰
const unlockCheck_p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(unlockCheckTxComposer, utxos);
// 4.3 往解锁合约交易中添加解锁输出(重要)
const unlockCheckOutputIndex = (0, transactionHelpers_1.addContractOutput)({
txComposer: unlockCheckTxComposer,
lockingScript: unlockContract.lockingScript,
dustCalculator: this.dustCalculator,
});
// 4.4 解锁交易找零
let changeOutputIndex = (0, transactionHelpers_1.addChangeOutput)(unlockCheckTxComposer, middleChangeAddress, this.feeb);
(0, transactionHelpers_1.unlockP2PKHInputs)(unlockCheckTxComposer, unlockCheck_p2pkhInputIndexes, utxoPrivateKeys);
// 4.5 检查费率
(0, transactionHelpers_1.checkFeeRate)(unlockCheckTxComposer, this.feeb);
// 4.6 重新集结此次操作后的钱
utxos = [
{
txId: unlockCheckTxComposer.getTxId(),
satoshis: unlockCheckTxComposer.getOutput(changeOutputIndex).satoshis,
outputIndex: changeOutputIndex,
address: middleChangeAddress,
},
];
utxoPrivateKeys = utxos.map((v) => middlePrivateKey).filter((v) => v);
// 4.7 构建解锁交易的Utxo
let unlockCheckUtxo = {
txId: unlockCheckTxComposer.getTxId(),
outputIndex: unlockCheckOutputIndex,
satoshis: unlockCheckTxComposer.getOutput(unlockCheckOutputIndex).satoshis,
lockingScript: unlockCheckTxComposer.getOutput(unlockCheckOutputIndex).script,
};
// 第五步:构建NFT转移交易
// 输入:1.销售 2.nft 3.钱 4.解锁合约
// 输出:1.nft 2.opreturn 3.找零 (相比于buy,没有发给销售者的所得)
// 转移合约交易构建器
const txComposer = new tx_composer_1.TxComposer();
let prevouts = new Prevouts_1.Prevouts();
// 5.1 放入销售输入
const sellInputIndex = txComposer.appendInput(nftSellUtxo);
prevouts.addVout(nftSellUtxo.txId, nftSellUtxo.outputIndex);
// 5.2 放入NFT输入
const nftInputIndex = txComposer.appendInput(nftInput);
prevouts.addVout(nftInput.txId, nftInput.outputIndex);
// 5.3 放入钱输入
const p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos);
utxos.forEach((utxo) => {
prevouts.addVout(utxo.txId, utxo.outputIndex);
});
// 5.4 放入解锁合约输入
const unlockCheckInputIndex = txComposer.appendInput(unlockCheckUtxo);
prevouts.addVout(unlockCheckUtxo.txId, unlockCheckUtxo.outputIndex);
// 5.5 重建销售合约
let nftSellContract = nftSell_1.NftSellFactory.createContract(new scryptlib_1.Ripemd160((0, scryptlib_1.toHex)(new mvc.Address(sellUtxo.sellerAddress, this.network).hashBuffer)), sellUtxo.price, new scryptlib_1.Bytes(codehash), new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftID)));
nftSellContract.setFormatedDataPart(nftSellProto.parseDataPart(nftSellUtxo.lockingScript.toBuffer()));
// 5.6 不存在的啦(不用打销售款)
// 5.7 添加nft输出
// 5.7.1 构造nft脚本(将nft的所有权转移给销售者)
const lockingScriptBuf = (0, contractHelpers_1.rebuildNftLockingScript)(nftInput, nftAddress);
// 5.7.2 添加进输出
const nftOutputIndex = (0, transactionHelpers_1.addContractOutput)({
txComposer,
lockingScript: mvc.Script.fromBuffer(lockingScriptBuf),
dustCalculator: this.dustCalculator,
});
// 5.8 添加opreturn输出
let opreturnScriptHex = '';
if (opreturnData) {
const opreturnOutputIndex = txComposer.appendOpReturnOutput(opreturnData);
opreturnScriptHex = txComposer.getOutput(opreturnOutputIndex).script.toHex();
}
// 5.9 解锁nft合约,并找零
for (let c = 0; c < 2; c++) {
/** 5.9.1 解锁NFT合约 */
txComposer.clearChangeOutput();
const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb);
const nftContract = nft_1.NftFactory.createContract(this.unlockContractCodeHashArray, codehash);
let dataPartObj = nftProto.parseDataPart(nftUtxo.lockingScript.toBuffer());
nftContract.setFormatedDataPart(dataPartObj);
// 准备数据
const prevNftInputIndex = nftUtxo.satotxInfo.preNftInputIndex;
const nftTx = new mvc.Transaction(nftUtxo.satotxInfo.txHex);
const inputRes = TokenUtil.getTxInputProof(nftTx, prevNftInputIndex);
const nftTxInputProof = new TxInputProof(inputRes[0]);
const nftTxHeader = inputRes[1];
const prevNftTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(nftUtxo.satotxInfo.preTx, nftUtxo.satotxInfo.preOutputIndex));
// 重要:解锁相关参数
const contractInputIndex = sellInputIndex;
const contractTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(nftSellTx, nftSellUtxo.outputIndex));
const amountCheckHashIndex = 1; // 对应out_6
const amountCheckInputIndex = unlockCheckInputIndex;
const unlockCheckTx = unlockCheckTxComposer.getTx();
const amountCheckTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(unlockCheckTx, unlockCheckOutputIndex));
const amountCheckScriptBuf = unlockCheckTx.outputs[unlockCheckOutputIndex].script.toBuffer();
const amountCheckScrypt = new scryptlib_1.Bytes(amountCheckScriptBuf.toString('hex'));
const unlockingContract = nftContract.unlock({
txPreimage: txComposer.getInputPreimage(nftInputIndex),
prevouts: new scryptlib_1.Bytes(prevouts.toHex()),
prevNftInputIndex,
prevNftAddress: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftUtxo.preNftAddress.hashBuffer)),
nftTxHeader,
nftTxInputProof,
prevNftTxProof,
genesisScript,
contractInputIndex,
contractTxProof,
amountCheckHashIndex,
amountCheckInputIndex,
amountCheckTxProof,
amountCheckScrypt,
operation: nftProto.NFT_OP_TYPE.UNLOCK_FROM_CONTRACT,
});
if (this.debug) {
let txContext = {
tx: txComposer.tx,
inputIndex: nftInputIndex,
inputSatoshis: txComposer.getInput(nftInputIndex).output.satoshis,
};
let ret = unlockingContract.verify(txContext);
if (ret.success == false)
throw ret;
}
txComposer.getInput(nftInputIndex).setScript(unlockingContract.toScript());
/** 5.9.1.5 其他输出 */
let otherOutputs = Buffer.alloc(0);
txComposer.tx.outputs.forEach((output, index) => {
if (index != nftOutputIndex) {
let outputBuf = output.toBufferWriter().toBuffer();
let lenBuf = Buffer.alloc(4);
lenBuf.writeUInt32LE(outputBuf.length);
otherOutputs = Buffer.concat([otherOutputs, lenBuf, outputBuf]);
}
});
/** 5.9.2 解锁检查合约 */
const nftOutputProof = (0, proofHelpers_1.createTxOutputProof)(nftTx, nftUtxo.satotxInfo.outputIndex);
let sub = unlockCheckUtxo.lockingScript;
sub = sub.subScript(0);
const txPreimage = new scryptlib_1.SigHashPreimage((0, scryptlib_1.toHex)((0, scryptlib_1.getPreimage)(txComposer.getTx(), sub, unlockCheckUtxo.satoshis, unlockCheckInputIndex)));
let unlockCall = unlockContract.unlock({
txPreimage,
prevouts: new scryptlib_1.Bytes(prevouts.toHex()),
nftInputIndex,
nftScript: new scryptlib_1.Bytes(nftInput.lockingScript.toHex()),
nftTxHeader: nftOutputProof.txHeader,
nftTxHashProof: nftOutputProof.hashProof,
nftSatoshiBytes: nftOutputProof.satoshiBytes,
nOutputs: txComposer.tx.outputs.length,
txNftOutputIndex: nftOutputIndex,
nftOutputAddress: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftAddress.hashBuffer)),
nftOutputSatoshis: txComposer.getOutput(nftOutputIndex).satoshis,
otherOutputArray: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(otherOutputs)),
});
if (this.debug) {
let txContext = {
tx: txComposer.getTx(),
inputIndex: unlockCheckInputIndex,
inputSatoshis: txComposer.getInput(unlockCheckInputIndex).output.satoshis,
};
let ret = unlockCall.verify(txContext);
if (ret.success == false)
throw ret;
}
txComposer.getInput(unlockCheckInputIndex).setScript(unlockCall.toScript());
/** 5.9.3 解锁销售合约 */
let sellUtxo = txComposer.getInput(sellInputIndex).output;
let sellSubScript = sellUtxo.script;
sellSubScript = sellSubScript.subScript(0);
const sellTxPreimage = new scryptlib_1.SigHashPreimage((0, scryptlib_1.toHex)((0, scryptlib_1.getPreimage)(txComposer.getTx(), sellSubScript, sellUtxo.satoshis, sellInputIndex, Signature.SIGHASH_SINGLE | Signature.SIGHASH_FORKID)));
const unlockCall2 = nftSellContract.unlock({
txPreimage: sellTxPreimage,
// 以下4个参数只有在cancelSell中才有
nftScript: new scryptlib_1.Bytes(nftInput.lockingScript.toHex()),
senderPubKey: new scryptlib_1.PubKey((0, scryptlib_1.toHex)(sellerPrivateKey.publicKey.toBuffer())),
senderSig: new scryptlib_1.Sig((0, scryptlib_1.toHex)(txComposer.getTxFormatSig(sellerPrivateKey, sellInputIndex))),
nftOutputSatoshis: txComposer.getOutput(nftOutputIndex).satoshis,
op: nftSell_1.NFT_SELL_OP.CANCEL,
});
if (this.debug) {
let txContext = {
tx: txComposer.getTx(),
inputIndex: sellInputIndex,
inputSatoshis: txComposer.getInput(sellInputIndex).output.satoshis,
};
let ret = unlockCall2.verify(txContext);
if (ret.success == false)
throw ret;
}
txComposer.getInput(sellInputIndex).setScript(unlockCall2.toScript());
}
// 6. 解锁输入,检查费率
(0, transactionHelpers_1.unlockP2PKHInputs)(txComposer, p2pkhInputIndexes, utxoPrivateKeys);
(0, transactionHelpers_1.checkFeeRate)(txComposer, this.feeb);
return { unlockCheckTxComposer, txComposer };
});
}
buy({ genesis, codehash, tokenIndex, buyerWif, sellUtxo, opreturnData, utxos: utxosInput, changeAddress, noBroadcast = false, middleChangeAddress, middleWif, publisherAddress, publisherFee, publisherFeeRate, creatorAddress, creatorFee, creatorFeeRate, }) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
// checkParamGenesis(genesis)
// checkParamCodehash(codehash)
// 准备钱💰
const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput);
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 this operation, please merge it first.');
}
const buyerPrivateKey = new mvc.PrivateKey(buyerWif);
// 准备找零地址
if (changeAddress) {
changeAddress = new mvc.Address(changeAddress, this.network);
}
else {
changeAddress = utxos[0].address;
}
// 准备中间找零地址
let middlePrivateKey;
if (middleChangeAddress) {
middleChangeAddress = new mvc.Address(middleChangeAddress, this.network);
middlePrivateKey = new mvc.PrivateKey(middleWif);
}
else {
middleChangeAddress = utxos[0].address;
middlePrivateKey = utxoPrivateKeys[0];
}
// 查找销售utxo
if (!sellUtxo) {
sellUtxo = yield this.api.getNftSellUtxo(codehash, genesis, tokenIndex);
}
if (!sellUtxo) {
throw new error_1.CodeError(error_1.ErrCode.EC_NFT_NOT_ON_SELL, 'The NFT is not for sale because the corresponding SellUtxo cannot be found.');
}
const price = sellUtxo.price;
// 检查发行者和创作者的地址和费率参数
this._checkRoyaltyParams({
price,
publisherAddress,
publisherFee,
publisherFeeRate,
creatorAddress,
creatorFee,
creatorFeeRate,
});
let { unlockCheckTxComposer, txComposer } = yield this.createBuyTx({
utxos,
utxoPrivateKeys,
genesis,
codehash,
tokenIndex,
sellUtxo,
buyerPrivateKey: buyerPrivateKey,
opreturnData,
changeAddress,
middlePrivateKey,
middleChangeAddress,
publisherAddress,
publisherFee,
publisherFeeRate,
creatorAddress,
creatorFee,
creatorFeeRate,
});
let unlockCheckTxHex = unlockCheckTxComposer.getRawHex();
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.api.broadcast(unlockCheckTxHex);
yield this.api.broadcast(txHex);
}
const runtime = Date.now() - startTime;
return {
tx: txComposer.tx,
txHex,
txid: txComposer.tx.id,
unlockCheckTxId: unlockCheckTxComposer.getTxId(),
unlockCheckTx: unlockCheckTxComposer.getTx(),
unlockCheckTxHex: unlockCheckTxHex,
runtime,
};
});
}
createBuyTx({ utxos, utxoPrivateKeys, genesis, codehash, tokenIndex, sellUtxo, buyerPrivateKey, opreturnData, changeAddress, middlePrivateKey, middleChangeAddress, publisherAddress, publisherFee, publisherFeeRate, creatorAddress, creatorFee, creatorFeeRate, }) {
return __awaiter(this, void 0, void 0, function* () {
// 第一步:找回并准备NFT Utxo
// 1.1 找回nft Utxo
let { nftUtxo } = yield (0, transactionHelpers_1.getNftInfo)({
tokenIndex,
codehash,
genesis,
api: this.api,
network: this.network,
});
// 1.2 验证nft Utxo
nftUtxo = yield this.pretreatNftUtxo(nftUtxo, codehash, genesis);
// 第二步:找到并重建销售utxo
// 2.1 查找销售utxo的步骤在上面已经完成(为了拿到价格,进行版税费用检查)
// 2.2 重建销售utxo
let nftSellTxHex = yield this.api.getRawTxData(sellUtxo.txId);
let nftSellTx = new mvc.Transaction(nftSellTxHex);
let nftSellUtxo = {
txId: sellUtxo.txId,
outputIndex: sellUtxo.outputIndex,
satoshis: nftSellTx.outputs[sellUtxo.outputIndex].satoshis,
lockingScript: nftSellTx.outputs[sellUtxo.outputIndex].script,
};
// 第三步:确保余额充足(需要构造三个交易)
const genesisScript = new scryptlib_1.Bytes(nftUtxo.preLockingScript.toHex());
let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0);
let estBuyFee = yield this._calBuyEstimateFee({
codehash,
nftUtxoSatoshis: nftUtxo.satoshis,
nftSellUtxo,
sellUtxo,
genesisScript,
utxoMaxCount: utxos.length,
opreturnData,
});
if (balance < estBuyFee) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estBuyFee}, but only ${balance}.`);
}
// 第四步:构建解锁交易
// 4.1 准备nft解锁数据
let nftInput = nftUtxo;
let nftID = nftProto.getNftID(nftInput.lockingScript.toBuffer());
let unlockContract = nftUnlockContractCheck_1.NftUnlockContractCheckFactory.createContract(nftUnlockContractCheck_1.NFT_UNLOCK_CONTRACT_TYPE.OUT_6);
unlockContract.setFormatedDataPart({
nftCodeHash: Buffer.from(codehash, 'hex'),
nftID,
});
// 解锁合约交易构建器
const unlockCheckTxComposer = new tx_composer_1.TxComposer();
// 4.2 往解锁合约交易中塞钱💰
const unlockCheck_p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(unlockCheckTxComposer, utxos);
// 4.3 往解锁合约交易中添加解锁输出(重要)
const unlockCheckOutputIndex = (0, transactionHelpers_1.addContractOutput)({
txComposer: unlockCheckTxComposer,
lockingScript: unlockContract.lockingScript,
dustCalculator: this.dustCalculator,
});
// 4.4 解锁交易找零
let changeOutputIndex = (0, transactionHelpers_1.addChangeOutput)(unlockCheckTxComposer, middleChangeAddress, this.feeb);
(0, transactionHelpers_1.unlockP2PKHInputs)(unlockCheckTxComposer, unlockCheck_p2pkhInputIndexes, utxoPrivateKeys);
// 4.5 检查费率
(0, transactionHelpers_1.checkFeeRate)(unlockCheckTxComposer, this.feeb);
// 4.6 重新集结此次操作后的钱
utxos = [
{
txId: unlockCheckTxComposer.getTxId(),
satoshis: unlockCheckTxComposer.getOutput(changeOutputIndex).satoshis,
outputIndex: changeOutputIndex,
address: middleChangeAddress,
},
];
utxoPrivateKeys = utxos.map((v) => middlePrivateKey).filter((v) => v);
// 4.7 构建解锁交易的Utxo
let unlockCheckUtxo = {
txId: unlockCheckTxComposer.getTxId(),
outputIndex: unlockCheckOutputIndex,
satoshis: unlockCheckTxComposer.getOutput(unlockCheckOutputIndex).satoshis,
lockingScript: unlockCheckTxComposer.getOutput(unlockCheckOutputIndex).script,
};
// 第五步:构建NFT转移交易
// 输入:1.销售 2.nft 3.钱 4.解锁合约
// 输出:1.销售者所得 (1.5 版税:发行者、创作者) 2.nft 3.opreturn 4.找零
// 转移合约交易构建器
const txComposer = new tx_composer_1.TxComposer();
let prevouts = new Prevouts_1.Prevouts();
// 5.1 放入销售输入
const sellInputIndex = txComposer.appendInput(nftSellUtxo);
prevouts.addVout(nftSellUtxo.txId, nftSellUtxo.outputIndex);
// 5.2 放入NFT输入
const nftInputIndex = txComposer.appendInput(nftInput);
prevouts.addVout(nftInput.txId, nftInput.outputIndex);
// 5.3 放入钱输入
const p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos);
utxos.forEach((utxo) => {
prevouts.addVout(utxo.txId, utxo.outputIndex);
});
// 5.4 放入解锁合约输入
const unlockCheckInputIndex = txComposer.appendInput(unlockCheckUtxo);
prevouts.addVout(unlockCheckUtxo.txId, unlockCheckUtxo.outputIndex);
// 5.5 重建销售合约
let nftSellContract = nftSell_1.NftSellFactory.createContract(new scryptlib_1.Ripemd160((0, scryptlib_1.toHex)(new mvc.Address(sellUtxo.sellerAddress, this.network).hashBuffer)), sellUtxo.price, new scryptlib_1.Bytes(codehash), new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftID)));
const parsed = nftSellProto.parseDataPart(nftSellUtxo.lockingScript.toBuffer());
nftSellContract.setFormatedDataPart(parsed);
// 5.6 取得销售者地址,将销售所得构建输出
const sellerAddress = mvc.Address.fromPublicKeyHash(Buffer.from(nftSellContract.constuctParams.senderAddress.value, 'hex'), this.network);
const sellerSatoshis = nftSellContract.constuctParams.bsvRecAmount;
txComposer.appendP2PKHOutput({
address: sellerAddress,
satoshis: sellerSatoshis,
});
// 5.6.5 版税:发行者、创作者
if (publisherAddress) {
// 有发行者地址,则根据费用或费率构建发行者费用输出
const publisherAmount = publisherFee || Math.ceil(sellerSatoshis * publisherFeeRate);
txComposer.appendP2PKHOutput({
address: new mvc.Address(publisherAddress, this.network),
satoshis: publisherAmount,
});
}
if (creatorAddress) {
// 有创作者地址,则根据费用或费率构建创作者费用输出
const creatorAmount = creatorFee || Math.ceil(sellerSatoshis * creatorFeeRate);
txComposer.appendP2PKHOutput({
address: new mvc.Address(creatorAddress, this.network),
satoshis: creatorAmount,
});
}
// 5.7 添加nft输出
// 5.7.1 构造nft脚本(将nft的所有权转移给买家)
const buyerAddress = buyerPrivateKey.toAddress(this.network);
const lockingScriptBuf = (0, contractHelpers_1.rebuildNftLockingScript)(nftInput, buyerAddress);
// 5.7.2 添加进输出
const nftOutputIndex = (0, transactionHelpers_1.addContractOutput)({
txComposer,
lockingScript: mvc.Script.fromBuffer(lockingScriptBuf),
dustCalculator: this.dustCalculator,
});
// 5.8 添加opreturn输出
let opreturnScriptHex = '';
if (opreturnData) {
const opreturnOutputIndex = txComposer.appendOpReturnOutput(opreturnData);
opreturnScriptHex = txComposer.getOutput(opreturnOutputIndex).script.toHex();
}
// 5.9 解锁nft合约,并找零
for (let c = 0; c < 2; c++) {
/** 5.9.1 解锁NFT合约 */
txComposer.clearChangeOutput();
const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb);
const nftContract = nft_1.NftFactory.createContract(this.unlockContractCodeHashArray, codehash);
let dataPartObj = nftProto.parseDataPart(nftUtxo.lockingScript.toBuffer());
nftContract.setFormatedDataPart(dataPartObj);
// 准备数据
const prevNftInputIndex = nftUtxo.satotxInfo.preNftInputIndex;
const nftTx = new mvc.Transaction(nftUtxo.satotxInfo.txHex);
const inputRes = TokenUtil.getTxInputProof(nftTx, prevNftInputIndex);
const nftTxInputProof = new TxInputProof(inputRes[0]);
const nftTxHeader = inputRes[1];
const prevNftTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(nftUtxo.satotxInfo.preTx, nftUtxo.satotxInfo.preOutputIndex));
// 重要:解锁相关参数
const contractInputIndex = sellInputIndex;
const contractTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(nftSellTx, nftSellUtxo.outputIndex));
const amountCheckHashIndex = 1; // 对应out_6
const amountCheckInputIndex = unlockCheckInputIndex;
const unlockCheckTx = unlockCheckTxComposer.getTx();
const amountCheckTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(unlockCheckTx, unlockCheckOutputIndex));
const amountCheckScriptBuf = unlockCheckTx.outputs[unlockCheckOutputIndex].script.toBuffer();
const amountCheckScrypt = new scryptlib_1.Bytes(amountCheckScriptBuf.toString('hex'));
const unlockingContract = nftContract.unlock({
txPreimage: txComposer.getInputPreimage(nftInputIndex),
prevouts: new scryptlib_1.Bytes(prevouts.toHex()),
prevNftInputIndex,
prevNftAddress: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftUtxo.preNftAddress.hashBuffer)),
nftTxHeader,
nftTxInputProof,
prevNftTxProof,
genesisScript,
contractInputIndex,
contractTxProof,
amountCheckHashIndex,
amountCheckInputIndex,
amountCheckTxProof,
amountCheckScrypt,
operation: nftProto.NFT_OP_TYPE.UNLOCK_FROM_CONTRACT,
});
if (this.debug) {
let txContext = {
tx: txComposer.tx,
inputIndex: nftInputIndex,
inputSatoshis: txComposer.getInput(nftInputIndex).output.satoshis,
};
let ret = unlockingContract.verify(txContext);
if (ret.success == false)
throw ret;
}
txComposer.getInput(nftInputIndex).setScript(unlockingContract.toScript());
/** 5.9.1.5 其他输出 */
let otherOutputs = Buffer.alloc(0);
txComposer.tx.outputs.forEach((output, index) => {
if (index != nftOutputIndex) {
let outputBuf = output.toBufferWriter().toBuffer();
let lenBuf = Buffer.alloc(4);
lenBuf.writeUInt32LE(outputBuf.length);
otherOutputs = Buffer.concat([otherOutputs, lenBuf, outputBuf]);
}
});
/** 5.9.2 解锁检查合约 */
const nftOutputProof = (0, proofHelpers_1.createTxOutputProof)(nftTx, nftUtxo.satotxInfo.outputIndex);
let sub = unlockCheckUtxo.lockingScript;
sub = sub.subScript(0);
const txPreimage = new scryptlib_1.SigHashPreimage((0, scryptlib_1.toHex)((0, scryptlib_1.getPreimage)(txComposer.getTx(), sub, unlockCheckUtxo.satoshis, unlockCheckInputIndex)));
let unlockCall = unlockContract.unlock({
// txPreimage: txComposer.getInputPreimage(unlockCheckInputIndex),
txPreimage,
prevouts: new scryptlib_1.Bytes(prevouts.toHex()),
nftInputIndex,
nftScript: new scryptlib_1.Bytes(nftInput.lockingScript.toHex()),
nftTxHeader: nftOutputProof.txHeader,
nftTxHashProof: nftOutputProof.hashProof,
nftSatoshiBytes: nftOutputProof.satoshiBytes,
nOutputs: txComposer.tx.outputs.length,
txNftOutputIndex: nftOutputIndex,
nftOutputAddress: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(buyerAddress.hashBuffer)),
nftOutputSatoshis: txComposer.getOutput(nftOutputIndex).satoshis,
otherOutputArray: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(otherOutputs)),
});
if (this.debug) {
let txContext = {
tx: txComposer.getTx(),
inputIndex: unlockCheckInputIndex,
inputSatoshis: txComposer.getInput(unlockCheckInputIndex).output.satoshis,
};
let ret = unlockCall.verify(txContext);