From 9252edee69e351a221b680c43fc5fff6d38504dd Mon Sep 17 00:00:00 2001 From: opcatdev Date: Wed, 6 Nov 2024 15:38:24 +0800 Subject: [PATCH] Add NftParallelClosedMinter --- .../nft/nftParallelClosedMinter.json | 426 ++++++++++++++++++ .../nft/nftParallelClosedMinterProto.json | 42 ++ .../contracts/nft/nftParallelClosedMinter.ts | 171 +++++++ .../nft/nftParallelClosedMinterProto.ts | 37 ++ packages/smartcontracts/src/index.ts | 5 + .../tests/nft/parallelClosedMinter.ts | 113 +++++ .../tests/nft/parallelclosedMinter.test.ts | 424 +++++++++++++++++ 7 files changed, 1218 insertions(+) create mode 100644 packages/smartcontracts/artifacts/contracts/nft/nftParallelClosedMinter.json create mode 100644 packages/smartcontracts/artifacts/contracts/nft/nftParallelClosedMinterProto.json create mode 100644 packages/smartcontracts/src/contracts/nft/nftParallelClosedMinter.ts create mode 100644 packages/smartcontracts/src/contracts/nft/nftParallelClosedMinterProto.ts create mode 100644 packages/smartcontracts/tests/nft/parallelClosedMinter.ts create mode 100644 packages/smartcontracts/tests/nft/parallelclosedMinter.test.ts diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftParallelClosedMinter.json b/packages/smartcontracts/artifacts/contracts/nft/nftParallelClosedMinter.json new file mode 100644 index 0000000..f5c8c93 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftParallelClosedMinter.json @@ -0,0 +1,426 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftParallelClosedMinter", + "md5": "12afc68fd84cddd24bc866da2955fb57", + "structs": [ + { + "name": "SHPreimage", + "params": [ + { + "name": "txVer", + "type": "bytes" + }, + { + "name": "nLockTime", + "type": "bytes" + }, + { + "name": "hashPrevouts", + "type": "bytes" + }, + { + "name": "hashSpentAmounts", + "type": "bytes" + }, + { + "name": "hashSpentScripts", + "type": "bytes" + }, + { + "name": "hashSequences", + "type": "bytes" + }, + { + "name": "hashOutputs", + "type": "bytes" + }, + { + "name": "spendType", + "type": "bytes" + }, + { + "name": "inputIndex", + "type": "bytes" + }, + { + "name": "hashTapLeaf", + "type": "bytes" + }, + { + "name": "keyVer", + "type": "bytes" + }, + { + "name": "codeSeparator", + "type": "bytes" + }, + { + "name": "_e", + "type": "bytes" + }, + { + "name": "eLastByte", + "type": "int" + } + ], + "genericTypes": [] + }, + { + "name": "PrevoutsCtx", + "params": [ + { + "name": "prevouts", + "type": "bytes[6]" + }, + { + "name": "inputIndexVal", + "type": "int" + }, + { + "name": "outputIndexVal", + "type": "int" + }, + { + "name": "spentTxhash", + "type": "bytes" + }, + { + "name": "outputIndex", + "type": "bytes" + } + ], + "genericTypes": [] + }, + { + "name": "BacktraceInfo", + "params": [ + { + "name": "preTx", + "type": "XrayedTxIdPreimg1" + }, + { + "name": "preTxInput", + "type": "TxInput" + }, + { + "name": "preTxInputIndex", + "type": "int" + }, + { + "name": "prePreTx", + "type": "XrayedTxIdPreimg2" + } + ], + "genericTypes": [] + }, + { + "name": "CAT721State", + "params": [ + { + "name": "ownerAddr", + "type": "bytes" + }, + { + "name": "localId", + "type": "int" + } + ], + "genericTypes": [] + }, + { + "name": "NftParallelClosedMinterState", + "params": [ + { + "name": "nftScript", + "type": "bytes" + }, + { + "name": "nextLocalId", + "type": "int" + } + ], + "genericTypes": [] + }, + { + "name": "PreTxStatesInfo", + "params": [ + { + "name": "statesHashRoot", + "type": "bytes" + }, + { + "name": "txoStateHashes", + "type": "bytes[5]" + } + ], + "genericTypes": [] + }, + { + "name": "XrayedTxIdPreimg1", + "params": [ + { + "name": "version", + "type": "bytes" + }, + { + "name": "inputCount", + "type": "bytes" + }, + { + "name": "inputs", + "type": "bytes[6]" + }, + { + "name": "outputCountVal", + "type": "int" + }, + { + "name": "outputCount", + "type": "bytes" + }, + { + "name": "outputSatoshisList", + "type": "bytes[6]" + }, + { + "name": "outputScriptList", + "type": "bytes[6]" + }, + { + "name": "nLocktime", + "type": "bytes" + } + ], + "genericTypes": [] + }, + { + "name": "XrayedTxIdPreimg2", + "params": [ + { + "name": "prevList", + "type": "bytes[4]" + }, + { + "name": "outputCountVal", + "type": "int" + }, + { + "name": "outputCount", + "type": "bytes" + }, + { + "name": "outputSatoshisList", + "type": "bytes[6]" + }, + { + "name": "outputScriptList", + "type": "bytes[6]" + }, + { + "name": "nLocktime", + "type": "bytes" + } + ], + "genericTypes": [] + }, + { + "name": "XrayedTxIdPreimg3", + "params": [ + { + "name": "prev", + "type": "bytes" + }, + { + "name": "outputCountVal", + "type": "int" + }, + { + "name": "outputCount", + "type": "bytes" + }, + { + "name": "outputSatoshisList", + "type": "bytes[4]" + }, + { + "name": "outputScriptList", + "type": "bytes[4]" + }, + { + "name": "nLocktime", + "type": "bytes" + } + ], + "genericTypes": [] + }, + { + "name": "TxInput", + "params": [ + { + "name": "txhash", + "type": "bytes" + }, + { + "name": "outputIndex", + "type": "bytes" + }, + { + "name": "outputIndexVal", + "type": "int" + }, + { + "name": "sequence", + "type": "bytes" + } + ], + "genericTypes": [] + }, + { + "name": "ChangeInfo", + "params": [ + { + "name": "script", + "type": "bytes" + }, + { + "name": "satoshis", + "type": "bytes" + } + ], + "genericTypes": [] + } + ], + "library": [ + { + "name": "SigHashUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "Backtrace", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "CAT721Proto", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "NftParallelClosedMinterProto", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "StateUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "TxProof", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "TxUtil", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "function", + "name": "mint", + "index": 0, + "params": [ + { + "name": "curTxoStateHashes", + "type": "bytes[5]" + }, + { + "name": "nftMint", + "type": "CAT721State" + }, + { + "name": "issuerPubKeyPrefix", + "type": "bytes" + }, + { + "name": "issuerPubKey", + "type": "PubKey" + }, + { + "name": "issuerSig", + "type": "Sig" + }, + { + "name": "minterSatoshis", + "type": "bytes" + }, + { + "name": "nftSatoshis", + "type": "bytes" + }, + { + "name": "preState", + "type": "NftParallelClosedMinterState" + }, + { + "name": "preTxStatesInfo", + "type": "PreTxStatesInfo" + }, + { + "name": "backtraceInfo", + "type": "BacktraceInfo" + }, + { + "name": "shPreimage", + "type": "SHPreimage" + }, + { + "name": "prevoutsCtx", + "type": "PrevoutsCtx" + }, + { + "name": "spentScripts", + "type": "bytes[6]" + }, + { + "name": "changeInfo", + "type": "ChangeInfo" + } + ] + }, + { + "type": "constructor", + "params": [ + { + "name": "ownerAddress", + "type": "bytes" + }, + { + "name": "genesisOutpoint", + "type": "bytes" + }, + { + "name": "max", + "type": "int" + } + ] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftParallelClosedMinter.scrypt", + "hex": "0800000000000000002079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984c807bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179842f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a03100000000005279567a757171557a78557a75547a547a547a547a76547a7572537a6d750126790126790126790126790126790126790126790126790126790126790126790126790126790126790111795e797e5d797e5c797e5b797e5a797e59797e58797e57797e56797e55797e54797e53797ea8011379787ea85279017f9f695279009c6301006752796878557952797e8801167955797e54798b7e6b6d6d6d6d6d6d6d6d6c775679ad011879011879011879011879011879011879011879011879011879011879012e790129795b795b795b795b795b795b790056766b796c756e7e777755766b796c756e7e777754766b796c756e7e777753766b796c756e7e777752766b796c756e7e777751766b796c756e7e7b756b6d6d6d6c77a852798855796e760087630100776876030000007e527987777777695479537978760087630100776876030000007e527987777777695b795b795b795b795b795b79565c797600a26976569f69948c766b796c756b6d6d6d6c547954797e886d6d6d6d6d6d5e795e795e795e795e795e790128795679567956795679567956790056766b796c756e827752797e7e777755766b796c756e827752797e7e777754766b796c756e827752797e7e777753766b796c756e827752797e7e777752766b796c756e827752797e7e777751766b796c756e827752797e7e7b756b6d6d6d6c77a878886d6d6d75015b79015b79015b79015b79015b79015b790163790163796e7ea97777014c79014c79014c79014c79014c79014c7956007600a26976569f69948c766b796c756b6d6d6d6c0119795879066a1863617401787e77527988577957795779577957795d79007657766b796c75a97e7d7756766b796c75a97e7d7755766b796c75a97e7d7754766b796c75a97e7d7753766b796c75a97e7d77a95279876b6d6d6d6c77695279587958795879587958795557798c7600a26976559f69948c766b796c756b6d6d756c886d6d6d6d75011279009d5e0113797600a26976569f6994766b796c750111790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790132790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790116790116797e7601167901167901167901167901167901167955766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167954766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167953766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167952766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167951766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167900766b796c756b6d6d6d6c7e7d775f797e775f795f79885d795d795d795d795d795d7955766b796c756b6d6d6d6c58795879587958795879587955766b796c756b6d6d6d6c768277000113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7954766b796c756b6d6d6d6c58795879587958795879587954766b796c756b6d6d6d6c768277510113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7953766b796c756b6d6d6d6c58795879587958795879587953766b796c756b6d6d6d6c768277520113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7952766b796c756b6d6d6d6c58795879587958795879587952766b796c756b6d6d6d6c768277530113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7951766b796c756b6d6d6d6c58795879587958795879587951766b796c756b6d6d6d6c768277540113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7900766b796c756b6d6d6d6c58795879587958795879587900766b796c756b6d6d6d6c768277550113799f637052797e53797e7e547a7572537a537975686d787752797eaa6b6d6d6d6d6d6d6d6d6d6d6d6d6c88011979011979011979011979707e01007e787e6b6d6d6c012f79012f79012f79012f79012f79012f7956011d797600a26976569f69948c766b796c756b6d6d6d6c8801177901197978760087630100776876030000007e527987777777690119790119797e7653798764011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579012d79012c79011679011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579007601147901147901147901147953766b796c756b6d6d6c7e7d7701147901147901147901147952766b796c756b6d6d6c7e7d7701147901147901147901147951766b796c756b6d6d6c7e7d7701147901147901147901147900766b796c756b6d6d6c7e775f795f79885d795d795d795d795d795d7955766b796c756b6d6d6d6c58795879587958795879587955766b796c756b6d6d6d6c768277000113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7954766b796c756b6d6d6d6c58795879587958795879587954766b796c756b6d6d6d6c768277510113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7953766b796c756b6d6d6d6c58795879587958795879587953766b796c756b6d6d6d6c768277520113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7952766b796c756b6d6d6d6c58795879587958795879587952766b796c756b6d6d6d6c768277530113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7951766b796c756b6d6d6d6c58795879587958795879587951766b796c756b6d6d6d6c768277540113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7900766b796c756b6d6d6d6c58795879587958795879587900766b796c756b6d6d6d6c768277550113799f637052797e53797e7e547a7572537a537975686d787752797eaa6b6d6d6d6d6d6d6d6d6d6d6c5379885979597959795979597959795658797600a26976569f69948c766b796c756b6d6d6d6c78886d6d6d6d6d6d6d6d6d6d6d686d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d75013f79013f797e7654798763015c79015c79015c79015c79015c7955517600a26976559f69948c766b796c756b6d6d756c0088015c79015c79015c79015c79015c7955527600a26976559f69948c766b796c756b6d6d756c0088015c79015c79015c79015c79015c7955537600a26976559f69948c766b796c756b6d6d756c0088015c79015c79015c79015c79015c7955547600a26976559f69948c766b796c756b6d6d756c008868000000016179016279938b0162790163799352937858799f635379577901687978827d770122a1696e7e53797e7777777e547a7572537a537975547901657953796e7ea97777a97e557a75547a547a547a547a54797552798b537a757b7b527975687658799f635379577901687978827d770122a1696e7e53797e7777777e547a7572537a537975547901657952796e7ea97777a97e557a75547a547a547a547a54797552798b537a757b7b52797568016a790164799d5479016c79016c7978827701149d6e7ea97777a97e557a75547a547a547a547a54797501647901667978827d770122a1696e7e53797e77777753798b547a7572537a5379755579547901737901737901737901737901737956795679557894000052799f637600a97e77685152799f637600a97e77685252799f637600a97e77685352799f637600a97e77685452799f637600a97e776877777ea9557955795579557955795579007657766b796c75a97e7d7756766b796c75a97e7d7755766b796c75a97e7d7754766b796c75a97e7d7753766b796c75a97e7d77a95279876b6d6d6d6c776976066a1863617401787e770800000000000000007882777e787e6b6d6d6d6d6c770111790111797601127987646e78827d770122a1696e7e53797e77777767006877777857797e53797e787ea876012c79885d79016e79016e797ea988016b79016d79ac6b6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6c77", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftParallelClosedMinterProto.json b/packages/smartcontracts/artifacts/contracts/nft/nftParallelClosedMinterProto.json new file mode 100644 index 0000000..ccd5d30 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftParallelClosedMinterProto.json @@ -0,0 +1,42 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftParallelClosedMinterProto", + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "structs": [ + { + "name": "NftParallelClosedMinterState", + "params": [ + { + "name": "nftScript", + "type": "bytes" + }, + { + "name": "nextLocalId", + "type": "int" + } + ], + "genericTypes": [] + } + ], + "library": [ + { + "name": "NftParallelClosedMinterProto", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "constructor", + "params": [] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftParallelClosedMinterProto.scrypt", + "hex": "", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/src/contracts/nft/nftParallelClosedMinter.ts b/packages/smartcontracts/src/contracts/nft/nftParallelClosedMinter.ts new file mode 100644 index 0000000..9220f1a --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftParallelClosedMinter.ts @@ -0,0 +1,171 @@ +import { + method, + SmartContract, + assert, + prop, + ByteString, + sha256, + PubKey, + Sig, + hash160, + toByteString, +} from 'scrypt-ts' +import { TxUtil, ChangeInfo, STATE_OUTPUT_INDEX, int32 } from '../utils/txUtil' +import { + PrevoutsCtx, + SHPreimage, + SigHashUtils, + SpentScriptsCtx, +} from '../utils/sigHashUtils' +import { Backtrace, BacktraceInfo } from '../utils/backtrace' +import { + NftParallelClosedMinterState, + NftParallelClosedMinterProto, +} from './nftParallelClosedMinterProto' +import { + PreTxStatesInfo, + StateUtils, + TxoStateHashes, +} from '../utils/stateUtils' +import { CAT721Proto, CAT721State } from './cat721Proto' + +export class NftParallelClosedMinter extends SmartContract { + @prop() + issuerAddress: ByteString + + @prop() + genesisOutpoint: ByteString + + @prop() + max: int32 + + constructor( + ownerAddress: ByteString, + genesisOutpoint: ByteString, + max: int32 + ) { + super(...arguments) + this.issuerAddress = ownerAddress + this.genesisOutpoint = genesisOutpoint + this.max = max + } + + @method() + public mint( + curTxoStateHashes: TxoStateHashes, + // contrat logic args + nftMint: CAT721State, + issuerPubKeyPrefix: ByteString, + issuerPubKey: PubKey, + issuerSig: Sig, + // contract lock satoshis + minterSatoshis: ByteString, + nftSatoshis: ByteString, + // verify preTx data part + preState: NftParallelClosedMinterState, + preTxStatesInfo: PreTxStatesInfo, + + // backtrace + backtraceInfo: BacktraceInfo, + + // common args + // current tx info + shPreimage: SHPreimage, + prevoutsCtx: PrevoutsCtx, + spentScripts: SpentScriptsCtx, + // change output info + changeInfo: ChangeInfo + ) { + // check preimage + assert( + this.checkSig( + SigHashUtils.checkSHPreimage(shPreimage), + SigHashUtils.Gx + ), + 'preimage check error' + ) + // check ctx + SigHashUtils.checkPrevoutsCtx( + prevoutsCtx, + shPreimage.hashPrevouts, + shPreimage.inputIndex + ) + SigHashUtils.checkSpentScriptsCtx( + spentScripts, + shPreimage.hashSpentScripts + ) + // verify state + StateUtils.verifyPreStateHash( + preTxStatesInfo, + NftParallelClosedMinterProto.stateHash(preState), + backtraceInfo.preTx.outputScriptList[STATE_OUTPUT_INDEX], + prevoutsCtx.outputIndexVal + ) + // minter need at input 0 + assert(prevoutsCtx.inputIndexVal == 0n) + // check preTx script eq this locking script + const preScript = spentScripts[Number(prevoutsCtx.inputIndexVal)] + // back to genesis + Backtrace.verifyUnique( + prevoutsCtx.spentTxhash, + backtraceInfo, + this.genesisOutpoint, + preScript + ) + // is genesis + const prevOutpoint = + backtraceInfo.preTxInput.txhash + + backtraceInfo.preTxInput.outputIndex + if (prevOutpoint == this.genesisOutpoint) { + // genesis only deploy one minter + assert(preTxStatesInfo.txoStateHashes[1] == toByteString('')) + assert(preTxStatesInfo.txoStateHashes[2] == toByteString('')) + assert(preTxStatesInfo.txoStateHashes[3] == toByteString('')) + assert(preTxStatesInfo.txoStateHashes[4] == toByteString('')) + } + let hashString = toByteString('') + let minterOutput = toByteString('') + let stateNumber = 0n + // next 1 + const nextLocalId1 = preState.nextLocalId + preState.nextLocalId + 1n + // next 2 + const nextLocalId2 = preState.nextLocalId + preState.nextLocalId + 2n + if (nextLocalId1 < this.max) { + minterOutput += TxUtil.buildOutput(preScript, minterSatoshis) + hashString += hash160( + NftParallelClosedMinterProto.stateHash({ + nftScript: preState.nftScript, + nextLocalId: nextLocalId1, + }) + ) + stateNumber += 1n + } + if (nextLocalId2 < this.max) { + minterOutput += TxUtil.buildOutput(preScript, minterSatoshis) + hashString += hash160( + NftParallelClosedMinterProto.stateHash({ + nftScript: preState.nftScript, + nextLocalId: nextLocalId2, + }) + ) + stateNumber += 1n + } + assert(nftMint.localId == preState.nextLocalId) + hashString += hash160(CAT721Proto.stateHash(nftMint)) + const nftOutput = TxUtil.buildOutput(preState.nftScript, nftSatoshis) + stateNumber += 1n + const stateOutput = StateUtils.getCurrentStateOutput( + hashString, + stateNumber, + curTxoStateHashes + ) + const changeOutput = TxUtil.getChangeOutput(changeInfo) + const hashOutputs = sha256( + stateOutput + minterOutput + nftOutput + changeOutput + ) + assert(hashOutputs == shPreimage.hashOutputs, 'hashOutputs mismatch') + // check sig + assert(this.issuerAddress == hash160(issuerPubKeyPrefix + issuerPubKey)) + assert(this.checkSig(issuerSig, issuerPubKey)) + } +} diff --git a/packages/smartcontracts/src/contracts/nft/nftParallelClosedMinterProto.ts b/packages/smartcontracts/src/contracts/nft/nftParallelClosedMinterProto.ts new file mode 100644 index 0000000..9363c68 --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftParallelClosedMinterProto.ts @@ -0,0 +1,37 @@ +import { + ByteString, + hash160, + int2ByteString, + method, + SmartContractLib, +} from 'scrypt-ts' +import { int32 } from '../utils/txUtil' + +export type NftParallelClosedMinterState = { + nftScript: ByteString + nextLocalId: int32 +} + +export class NftParallelClosedMinterProto extends SmartContractLib { + @method() + static stateHash(_state: NftParallelClosedMinterState): ByteString { + return hash160(_state.nftScript + int2ByteString(_state.nextLocalId)) + } + + static create( + nftScript: ByteString, + nextLocalId: int32 + ): NftParallelClosedMinterState { + return { + nftScript: nftScript, + nextLocalId: nextLocalId, + } + } + + static toByteString(closeMinterInfo: NftParallelClosedMinterState) { + return ( + closeMinterInfo.nftScript + + int2ByteString(closeMinterInfo.nextLocalId) + ) + } +} diff --git a/packages/smartcontracts/src/index.ts b/packages/smartcontracts/src/index.ts index c17e1e8..79821fe 100644 --- a/packages/smartcontracts/src/index.ts +++ b/packages/smartcontracts/src/index.ts @@ -13,12 +13,14 @@ import burnGuard from '../artifacts/contracts/token/burnGuard.json' import transferGuard from '../artifacts/contracts/token/transferGuard.json' import { NftClosedMinter } from './contracts/nft/nftClosedMinter' +import { NftParallelClosedMinter } from './contracts/nft/nftParallelClosedMinter' import { NftOpenMinter } from './contracts/nft/nftOpenMinter' import { CAT721 } from './contracts/nft/cat721' import { NftTransferGuard } from './contracts/nft/nftTransferGuard' import { NftBurnGuard } from './contracts/nft/nftBurnGuard' import nftClosedMinter from '../artifacts/contracts/nft/nftClosedMinter.json' +import nftParallelClosedMinter from '../artifacts/contracts/nft/nftParallelClosedMinter.json' import nftOpenMinter from '../artifacts/contracts/nft/nftOpenMinter.json' import cat721 from '../artifacts/contracts/nft/cat721.json' import nftTransferGuard from '../artifacts/contracts/nft/nftTransferGuard.json' @@ -34,6 +36,7 @@ import nftBurnGuard from '../artifacts/contracts/nft/nftBurnGuard.json' TransferGuard.loadArtifact(transferGuard) // nft minter NftClosedMinter.loadArtifact(nftClosedMinter) + NftParallelClosedMinter.loadArtifact(nftParallelClosedMinter) NftOpenMinter.loadArtifact(nftOpenMinter) // nft CAT721.loadArtifact(cat721) @@ -52,8 +55,10 @@ export * from './contracts/token/openMinterV2' export * from './contracts/token/openMinterProto' export * from './contracts/token/openMinterV2Proto' export * from './contracts/nft/nftClosedMinter' +export * from './contracts/nft/nftParallelClosedMinter' export * from './contracts/nft/nftOpenMinter' export * from './contracts/nft/nftClosedMinterProto' +export * from './contracts/nft/nftParallelClosedMinterProto' export * from './contracts/nft/nftOpenMinterProto' export * from './contracts/nft/nftOpenMinterMerkleTree' export * from './contracts/nft/cat721' diff --git a/packages/smartcontracts/tests/nft/parallelClosedMinter.ts b/packages/smartcontracts/tests/nft/parallelClosedMinter.ts new file mode 100644 index 0000000..49ea46e --- /dev/null +++ b/packages/smartcontracts/tests/nft/parallelClosedMinter.ts @@ -0,0 +1,113 @@ +import { NftClosedMinter } from '../../src/contracts/nft/nftClosedMinter' +import { + CatTx, + ContractCallResult, + ContractIns, + TaprootSmartContract, +} from '../../src/lib/catTx' +import { CAT721Proto, CAT721State } from '../../src/contracts/nft/cat721Proto' +import { + NftParallelClosedMinterProto, + NftParallelClosedMinterState, +} from '../../src/contracts/nft/nftParallelClosedMinterProto' + +export async function nftParallelClosedMinterDeploy( + seckey, + genesisUtxo, + nftClosedMinter: NftClosedMinter, + nftClosedMinterTaproot: TaprootSmartContract, + nftClosedMinterState: NftParallelClosedMinterState +): Promise> { + // tx deploy + const catTx = CatTx.create() + catTx.tx.from([genesisUtxo]) + const atIndex = catTx.addStateContractOutput( + nftClosedMinterTaproot.lockingScript, + NftParallelClosedMinterProto.toByteString(nftClosedMinterState) + ) + catTx.sign(seckey) + return { + catTx: catTx, + contract: nftClosedMinter, + state: nftClosedMinterState, + contractTaproot: nftClosedMinterTaproot, + atOutputIndex: atIndex, + } +} + +export async function nftParallelClosedMinterCall( + contractIns: ContractIns, + nftTaproot: TaprootSmartContract, + nftState: CAT721State, + max: bigint, + errorNextLocalId: boolean = false +): Promise> { + const catTx = CatTx.create() + const atInputIndex = catTx.fromCatTx( + contractIns.catTx, + contractIns.atOutputIndex + ) + const nexts: ContractIns[] = [] + // + let nextLocalId1 = + contractIns.state.nextLocalId + contractIns.state.nextLocalId + 1n + const nextLocalId2 = + contractIns.state.nextLocalId + contractIns.state.nextLocalId + 2n + if (errorNextLocalId) { + nextLocalId1 = nextLocalId1 + 1n + } + if (nextLocalId1 < max) { + const nextState = NftParallelClosedMinterProto.create( + contractIns.state.nftScript, + nextLocalId1 + ) + const atOutputIndex = catTx.addStateContractOutput( + contractIns.contractTaproot.lockingScript, + NftParallelClosedMinterProto.toByteString(nextState) + ) + nexts.push({ + catTx: catTx, + contract: contractIns.contract, + state: nextState, + contractTaproot: contractIns.contractTaproot, + atOutputIndex: atOutputIndex, + }) + } + if (nextLocalId2 < max) { + const nextState = NftParallelClosedMinterProto.create( + contractIns.state.nftScript, + nextLocalId2 + ) + const atOutputIndex = catTx.addStateContractOutput( + contractIns.contractTaproot.lockingScript, + NftParallelClosedMinterProto.toByteString(nextState) + ) + nexts.push({ + catTx: catTx, + contract: contractIns.contract, + state: nextState, + contractTaproot: contractIns.contractTaproot, + atOutputIndex: atOutputIndex, + }) + } + const atOutputIndex = catTx.addStateContractOutput( + contractIns.state.nftScript, + CAT721Proto.toByteString(nftState) + ) + nexts.push({ + catTx: catTx, + preCatTx: contractIns.catTx, + contract: nftTaproot.contract, + state: nftState, + contractTaproot: nftTaproot, + atOutputIndex: atOutputIndex, + }) + return { + catTx: catTx, + contract: contractIns.contract, + state: contractIns.state, + contractTaproot: contractIns.contractTaproot, + atInputIndex: atInputIndex, + nexts: nexts, + } +} diff --git a/packages/smartcontracts/tests/nft/parallelclosedMinter.test.ts b/packages/smartcontracts/tests/nft/parallelclosedMinter.test.ts new file mode 100644 index 0000000..adb0418 --- /dev/null +++ b/packages/smartcontracts/tests/nft/parallelclosedMinter.test.ts @@ -0,0 +1,424 @@ +import * as dotenv from 'dotenv' +dotenv.config() +import { expect, use } from 'chai' +import { NftParallelClosedMinter } from '../../src/contracts/nft/nftParallelClosedMinter' +import chaiAsPromised from 'chai-as-promised' +import { MethodCallOptions, hash160, toByteString } from 'scrypt-ts' +import { getOutpointString } from '../../src/lib/txTools' +import { + getDummyGenesisTx, + getDummySigner, + getDummyUTXO, +} from '../utils/txHelper' +import { getKeyInfoFromWif, getPrivKey } from '../utils/privateKey' +import { + nftParallelClosedMinterCall, + nftParallelClosedMinterDeploy, +} from './parallelClosedMinter' +import { + CatTx, + ContractCallResult, + ContractIns, + TaprootSmartContract, +} from '../../src/lib/catTx' +import { getBackTraceInfo } from '../../src/lib/proof' +import { unlockTaprootContractInput } from '../utils/contractUtils' +import { btc } from '../../src/lib/btc' +import { + NftParallelClosedMinterProto, + NftParallelClosedMinterState, +} from '../../src/contracts/nft/nftParallelClosedMinterProto' +import { CAT721Proto, CAT721State } from '../../src/contracts/nft/cat721Proto' +use(chaiAsPromised) + +const DUST = toByteString('4a01000000000000') + +export async function closedMinterUnlock( + callInfo: ContractCallResult, + preCatTx: CatTx, + seckey, + nftState, + preNftClosedMinterState, + pubkeyX, + pubKeyPrefix, + prePreTx, + options: { + errorSig?: boolean + } = {} +) { + const { shPreimage, prevoutsCtx, spentScripts, sighash } = + callInfo.catTx.getInputCtx( + callInfo.atInputIndex, + callInfo.contractTaproot.tapleafBuffer + ) + const backtraceInfo = getBackTraceInfo( + // pre + preCatTx.tx, + prePreTx, + callInfo.atInputIndex + ) + const sig = btc.crypto.Schnorr.sign(seckey, sighash.hash) + await callInfo.contract.connect(getDummySigner()) + const closedMinterFuncCall = await callInfo.contract.methods.mint( + callInfo.catTx.state.stateHashList, + nftState, + pubKeyPrefix, + pubkeyX, + () => (options.errorSig ? toByteString('') : sig.toString('hex')), + DUST, + DUST, + // pre state + preNftClosedMinterState, + preCatTx.getPreState(), + // + backtraceInfo, + shPreimage, + prevoutsCtx, + spentScripts, + { + script: toByteString(''), + satoshis: toByteString('0000000000000000'), + }, + { + fromUTXO: getDummyUTXO(), + verify: false, + exec: false, + } as MethodCallOptions + ) + unlockTaprootContractInput( + closedMinterFuncCall, + callInfo.contractTaproot, + callInfo.catTx.tx, + // pre tx + preCatTx.tx, + callInfo.atInputIndex, + true, + true + ) +} + +// keyInfo +const keyInfo = getKeyInfoFromWif(getPrivKey()) +const { addr: addrP2WPKH, seckey, xAddress, pubKeyPrefix, pubkeyX } = keyInfo +const { genesisTx, genesisUtxo } = getDummyGenesisTx(seckey, addrP2WPKH) +const genesisOutpoint = getOutpointString(genesisTx, 0) +const nftScript = + '5120c4043a44196c410dba2d7c9288869727227e8fcec717f73650c8ceadc90877cd' + +describe('Test SmartContract `NftParallelClosedMinter`', () => { + let nftClosedMinter: NftParallelClosedMinter + let nftClosedMinterTaproot: TaprootSmartContract + let initNftClosedMinterState: NftParallelClosedMinterState + let nftClosedMinterState: NftParallelClosedMinterState + let contractIns: ContractIns + const collectionMax = 100n + before(async () => { + await NftParallelClosedMinter.loadArtifact() + nftClosedMinter = new NftParallelClosedMinter( + xAddress, + genesisOutpoint, + collectionMax + ) + nftClosedMinterTaproot = TaprootSmartContract.create(nftClosedMinter) + initNftClosedMinterState = NftParallelClosedMinterProto.create( + nftScript, + 0n + ) + nftClosedMinterState = initNftClosedMinterState + contractIns = await nftParallelClosedMinterDeploy( + seckey, + genesisUtxo, + nftClosedMinter, + nftClosedMinterTaproot, + initNftClosedMinterState + ) + }) + + it('should admin mint nft pass.', async () => { + // tx call + // nft state + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + nftClosedMinterState.nextLocalId + ) + const callInfo = await nftParallelClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState, + collectionMax + ) + await closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + genesisTx + ) + expect(callInfo.nexts.length).to.be.equal(3) + }) + + it('should admin mint nft until end.', async () => { + // tx call + const nftList: ContractIns[] = [] + const parallelMinter = async function ( + contractIns: ContractIns, + prePreTx + ) { + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + contractIns.state.nextLocalId + ) + const callInfo = await nftParallelClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState, + collectionMax + ) + await closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx + ) + nftList.push( + callInfo.nexts[ + callInfo.nexts.length - 1 + ] as ContractIns + ) + if (callInfo.nexts.length > 1) { + for ( + let index = 0; + index < callInfo.nexts.length - 1; + index++ + ) { + await parallelMinter( + callInfo.nexts[ + index + ] as ContractIns, + contractIns.catTx.tx + ) + } + } + } + await parallelMinter(contractIns, genesisTx) + const localIdSet = new Set(nftList.map((nft) => nft.state.localId)) + expect(localIdSet.size).to.be.equal(Number(collectionMax)) + }) + + it('should failed mint nft with error localId', async () => { + // tx call + const nftList: ContractIns[] = [] + const parallelMinter = async function ( + contractIns: ContractIns, + prePreTx + ) { + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + contractIns.state.nextLocalId + 1n + ) + const callInfo = await nftParallelClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState, + collectionMax + ) + await expect( + closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx + ) + ).to.be.rejected + nftList.push( + callInfo.nexts[ + callInfo.nexts.length - 1 + ] as ContractIns + ) + if (callInfo.nexts.length > 1) { + for ( + let index = 0; + index < callInfo.nexts.length - 1; + index++ + ) { + await parallelMinter( + callInfo.nexts[ + index + ] as ContractIns, + contractIns.catTx.tx + ) + } + } + } + await parallelMinter(contractIns, genesisTx) + expect(nftList).to.be.length(Number(collectionMax)) + }) + + it('should failed mint nft with error nextLocalId', async () => { + it('should admin mint nft until end.', async () => { + // tx call + const nftList: ContractIns[] = [] + const parallelMinter = async function ( + contractIns: ContractIns, + prePreTx + ) { + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + contractIns.state.nextLocalId + ) + const callInfo = await nftParallelClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState, + collectionMax, + true + ) + await expect( + closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx + ) + ).to.be.rejected + nftList.push( + callInfo.nexts[ + callInfo.nexts.length - 1 + ] as ContractIns + ) + if (callInfo.nexts.length > 1) { + for ( + let index = 0; + index < callInfo.nexts.length - 1; + index++ + ) { + await parallelMinter( + callInfo.nexts[ + index + ] as ContractIns, + contractIns.catTx.tx + ) + } + } + } + await parallelMinter(contractIns, genesisTx) + expect(nftList).to.be.length(Number(collectionMax)) + }) + }) + + it('should failed mint nft with error sig', async () => { + // tx call + let prePreTx = genesisTx + while (nftClosedMinterState.nextLocalId <= collectionMax) { + // nft state + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + nftClosedMinterState.nextLocalId + ) + const callInfo = await nftParallelClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState, + collectionMax + ) + await expect( + closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx, + { + errorSig: true, + } + ) + ).to.be.rejected + prePreTx = contractIns.catTx.tx + if (callInfo.nexts.length > 1) { + contractIns = callInfo + .nexts[0] as ContractIns + } else { + break + } + nftClosedMinterState.nextLocalId += 1n + } + }) + + it('should failed genesis tx more than one minter', async () => { + async function nftParallelClosedMinterDeploy( + seckey, + genesisUtxo, + nftClosedMinter: NftParallelClosedMinter, + nftClosedMinterTaproot: TaprootSmartContract, + nftClosedMinterState: NftParallelClosedMinterState + ): Promise> { + // tx deploy + const catTx = CatTx.create() + catTx.tx.from([genesisUtxo]) + const atIndex = catTx.addStateContractOutput( + nftClosedMinterTaproot.lockingScript, + NftParallelClosedMinterProto.toByteString(nftClosedMinterState) + ) + catTx.addStateContractOutput( + nftClosedMinterTaproot.lockingScript, + NftParallelClosedMinterProto.toByteString(nftClosedMinterState) + ) + catTx.sign(seckey) + return { + catTx: catTx, + contract: nftClosedMinter, + state: nftClosedMinterState, + contractTaproot: nftClosedMinterTaproot, + atOutputIndex: atIndex, + } + } + const contractIns = await nftParallelClosedMinterDeploy( + seckey, + genesisUtxo, + nftClosedMinter, + nftClosedMinterTaproot, + initNftClosedMinterState + ) + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + nftClosedMinterState.nextLocalId + ) + const callInfo = await nftParallelClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState, + collectionMax + ) + await expect( + closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + genesisTx + ) + ).to.be.rejected + expect(callInfo.nexts.length).to.be.equal(3) + }) +})