diff --git a/packages/cli/package.json b/packages/cli/package.json index e8147b71..d0935e64 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,7 +28,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", - "@cat-protocol/cat-smartcontracts": "0.1.2", + "@cat-protocol/cat-smartcontracts": "0.2.1", "@types/inquirer": "^8.1.3", "bigi": "^1.4.2", "bip32": "^4.0.0", diff --git a/packages/cli/src/providers/configService.ts b/packages/cli/src/providers/configService.ts index cc692345..89816cc1 100644 --- a/packages/cli/src/providers/configService.ts +++ b/packages/cli/src/providers/configService.ts @@ -173,6 +173,7 @@ export class ConfigService { withProxy(options?: object) { if (this.getProxy()) { + options = options || {}; Object.assign(options, { agent: new HttpsProxyAgent(this.getProxy()), }); diff --git a/packages/smartcontracts/artifacts/contracts/nft/cat721.json b/packages/smartcontracts/artifacts/contracts/nft/cat721.json new file mode 100644 index 00000000..b7c19e47 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/cat721.json @@ -0,0 +1,446 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "CAT721", + "md5": "0176b77f091a0f5bb4ef9f4ee455302f", + "structs": [ + { + "name": "NftUnlockArgs", + "params": [ + { + "name": "isUserSpend", + "type": "bool" + }, + { + "name": "userPubKeyPrefix", + "type": "bytes" + }, + { + "name": "userPubKey", + "type": "PubKey" + }, + { + "name": "userSig", + "type": "Sig" + }, + { + "name": "contractInputIndex", + "type": "int" + } + ], + "genericTypes": [] + }, + { + "name": "NftGuardInfo", + "params": [ + { + "name": "tx", + "type": "XrayedTxIdPreimg3" + }, + { + "name": "inputIndexVal", + "type": "int" + }, + { + "name": "outputIndex", + "type": "bytes" + }, + { + "name": "guardState", + "type": "NftGuardConstState" + } + ], + "genericTypes": [] + }, + { + "name": "NftGuardConstState", + "params": [ + { + "name": "collectionScript", + "type": "bytes" + }, + { + "name": "localIdArray", + "type": "int[6]" + } + ], + "genericTypes": [] + }, + { + "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": "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": "NftGuardProto", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "SigHashUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "Backtrace", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "CAT721Proto", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "StateUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "TxProof", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "TxUtil", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "function", + "name": "unlock", + "index": 0, + "params": [ + { + "name": "nftUnlockArgs", + "type": "NftUnlockArgs" + }, + { + "name": "preState", + "type": "CAT721State" + }, + { + "name": "preTxStatesInfo", + "type": "PreTxStatesInfo" + }, + { + "name": "guardInfo", + "type": "NftGuardInfo" + }, + { + "name": "backtraceInfo", + "type": "BacktraceInfo" + }, + { + "name": "shPreimage", + "type": "SHPreimage" + }, + { + "name": "prevoutsCtx", + "type": "PrevoutsCtx" + }, + { + "name": "spentScriptsCtx", + "type": "bytes[6]" + } + ] + }, + { + "type": "constructor", + "params": [ + { + "name": "minterScript", + "type": "bytes" + }, + { + "name": "guardScript", + "type": "bytes" + } + ] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../cat721.scrypt", + "hex": "2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984c807bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179842f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031000001227901227901227901227901227901227901227901227901227901227901227901227901227901227960795e797e5d797e5c797e5b797e5a797e59797e58797e57797e56797e55797e54797e53797ea8011279787ea85279017f9f695279009c6301006752796878557952797e8801157955797e54798b7e6b6d6d6d6d6d6d6d6d6c775579ad011479011479011479011479011479011479011479011479011479011479012a790125795b795b795b795b795b795b790056766b796c756e7e777755766b796c756e7e777754766b796c756e7e777753766b796c756e7e777752766b796c756e7e777751766b796c756e7e7b756b6d6d6d6c77a852798855796e760087630100776876030000007e527987777777695479537978760087630100776876030000007e527987777777695b795b795b795b795b795b79565c797600a26976569f69948c766b796c756b6d6d6d6c547954797e886d6d6d6d6d6d5a795a795a795a795a795a790124795679567956795679567956790056766b796c756e827752797e7e777755766b796c756e827752797e7e777754766b796c756e827752797e7e777753766b796c756e827752797e7e777752766b796c756e827752797e7e777751766b796c756e827752797e7e7b756b6d6d6d6c77a878886d6d6d75016c79016c79016c79016c79016c79016c7901747901747978827701149d6e7ea9777701487901487901487901487901487901487956007600a26976569f69948c766b796c756b6d6d6d6c0115795879066a1863617401787e77527988577957795779577957795d79007657766b796c75a97e7d7756766b796c75a97e7d7755766b796c75a97e7d7754766b796c75a97e7d7753766b796c75a97e7d77a95279876b6d6d6d6c77695279587958795879587958795557798c7600a26976559f69948c766b796c756b6d6d756c886d6d6d6d755a5f797600a26976569f6994766b796c755d790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790153790132790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790116790116797e7601167901167901167901167901167901167955766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167954766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167953766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167952766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167951766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167900766b796c756b6d6d6d6c7e7d775f797e775f795f79885d795d795d795d795d795d7955766b796c756b6d6d6d6c58795879587958795879587955766b796c756b6d6d6d6c768277000113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7954766b796c756b6d6d6d6c58795879587958795879587954766b796c756b6d6d6d6c768277510113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7953766b796c756b6d6d6d6c58795879587958795879587953766b796c756b6d6d6d6c768277520113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7952766b796c756b6d6d6d6c58795879587958795879587952766b796c756b6d6d6d6c768277530113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7951766b796c756b6d6d6d6c58795879587958795879587951766b796c756b6d6d6d6c768277540113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7900766b796c756b6d6d6d6c58795879587958795879587900766b796c756b6d6d6d6c768277550113799f637052797e53797e7e547a7572537a537975686d787752797eaa6b6d6d6d6d6d6d6d6d6d6d6d6d6c88011979011979011979011979707e01007e787e6b6d6d6c012f79012f79012f79012f79012f79012f7956011d797600a26976569f69948c766b796c756b6d6d6d6c8801177901197978760087630100776876030000007e52798777777769011479011479011479011479011479011479011479011479011479011479011479011479011479011479011479011479011479011479011479007601147901147901147901147953766b796c756b6d6d6c7e7d7701147901147901147901147952766b796c756b6d6d6c7e7d7701147901147901147901147951766b796c756b6d6d6c7e7d7701147901147901147901147900766b796c756b6d6d6c7e775f795f79885d795d795d795d795d795d7955766b796c756b6d6d6d6c58795879587958795879587955766b796c756b6d6d6d6c768277000113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7954766b796c756b6d6d6d6c58795879587958795879587954766b796c756b6d6d6d6c768277510113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7953766b796c756b6d6d6d6c58795879587958795879587953766b796c756b6d6d6d6c768277520113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7952766b796c756b6d6d6d6c58795879587958795879587952766b796c756b6d6d6d6c768277530113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7951766b796c756b6d6d6d6c58795879587958795879587951766b796c756b6d6d6d6c768277540113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7900766b796c756b6d6d6d6c58795879587958795879587900766b796c756b6d6d6d6c768277550113799f637052797e53797e7e547a7572537a537975686d787752797eaa6b6d6d6d6d6d6d6d6d6d6d6c011a798858795879587958795879587956011e797600a26976569f69948c766b796c756b6d6d6d6c7653798778537987786476675168696d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d750167790167790167790167790167790167790167790167790167790167790167790167790167790167790167790167790167790167790167790167790167790115790285007902850079012779012e79012e79012e79012e79012e79012e79012a79012a79012a79012a79012a79012a7901167901167901167901167901167901167901167956797657795779577957795779577955766b796c756b6d6d6d6c7e7d7757795779577957795779577954766b796c756b6d6d6d6c7e7d7757795779577957795779577953766b796c756b6d6d6d6c7e7d7757795779577957795779577952766b796c756b6d6d6d6c7e7d7757795779577957795779577951766b796c756b6d6d6d6c7e7d7757795779577957795779577900766b796c756b6d6d6d6c7e7d77a96b6d6d6d6d6c765178a978557894000052799f637600a97e77685152799f637600a97e77685252799f637600a97e77685352799f637600a97e77685452799f637600a97e776877777ea9066a1863617401787e777777011f79011f79011f79011f7954007600a26976549f69948c766b796c756b6d6d6c88011779011179885e7900a269011679011679011679011679011679011679560114797600a26976569f69948c766b796c756b6d6d6d6c5f799d0125790125790125790125790125790125790125790125790125790125790125790125795a795a798800597959795979597953766b796c756b6d6d6c567956795679567953766b796c756b6d6d6c768277005f799f637052797e53797e7e547a7572537a537975686d75597959795979597952766b796c756b6d6d6c567956795679567952766b796c756b6d6d6c768277515f799f637052797e53797e7e547a7572537a537975686d75597959795979597951766b796c756b6d6d6c567956795679567951766b796c756b6d6d6c768277525f799f637052797e53797e7e547a7572537a537975686d75597959795979597900766b796c756b6d6d6c567956795679567900766b796c756b6d6d6c768277535f799f637052797e53797e7e547a7572537a537975686d755c79787e52797eaa6b6d6d6d6d6d6d6c775d011b797600a26976569f6994766b796c7578011b797e8857011b797600a26976569f6994766b796c75012979886d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d75017479630173790173797ea901707988017179017379ad67016f795c0172797600a26976569f6994766b796c75a988686d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d7551", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/cat721Proto.json b/packages/smartcontracts/artifacts/contracts/nft/cat721Proto.json new file mode 100644 index 00000000..4bb82ed9 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/cat721Proto.json @@ -0,0 +1,42 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "CAT721Proto", + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "structs": [ + { + "name": "CAT721State", + "params": [ + { + "name": "ownerAddr", + "type": "bytes" + }, + { + "name": "localId", + "type": "int" + } + ], + "genericTypes": [] + } + ], + "library": [ + { + "name": "CAT721Proto", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "constructor", + "params": [] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../cat721Proto.scrypt", + "hex": "", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftBurnGuard.json b/packages/smartcontracts/artifacts/contracts/nft/nftBurnGuard.json new file mode 100644 index 00000000..8a9f39b8 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftBurnGuard.json @@ -0,0 +1,337 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftBurnGuard", + "md5": "bdcfe0b013ecd9ec68098b8060234af4", + "structs": [ + { + "name": "NftGuardConstState", + "params": [ + { + "name": "collectionScript", + "type": "bytes" + }, + { + "name": "localIdArray", + "type": "int[6]" + } + ], + "genericTypes": [] + }, + { + "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": "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": "NftGuardProto", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "SigHashUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "StateUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "TxProof", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "TxUtil", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "function", + "name": "burn", + "index": 0, + "params": [ + { + "name": "curTxoStateHashes", + "type": "bytes[5]" + }, + { + "name": "outputScriptList", + "type": "bytes[5]" + }, + { + "name": "outputSatoshisList", + "type": "bytes[5]" + }, + { + "name": "preState", + "type": "NftGuardConstState" + }, + { + "name": "preTx", + "type": "XrayedTxIdPreimg3" + }, + { + "name": "shPreimage", + "type": "SHPreimage" + }, + { + "name": "prevoutsCtx", + "type": "PrevoutsCtx" + } + ] + }, + { + "type": "constructor", + "params": [] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftBurnGuard.scrypt", + "hex": "2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984c807bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179842f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a0310000011a79011a79011a79011a79011a79011a79011a79011a79011a79011a79011a79011a79011a79011a795e795e797e5d797e5c797e5b797e5a797e59797e58797e57797e56797e55797e54797e53797ea86079787ea85279017f9f695279009c6301006752796878557952797e8801137955797e54798b7e6b6d6d6d6d6d6d6d6d6c775379ad5c795c795c795c795c795c795c795c795c795c79012279011d795b795b795b795b795b795b790056766b796c756e7e777755766b796c756e7e777754766b796c756e7e777753766b796c756e7e777752766b796c756e7e777751766b796c756e7e7b756b6d6d6d6c77a852798855796e760087630100776876030000007e527987777777695479537978760087630100776876030000007e527987777777695b795b795b795b795b795b79565c797600a26976569f69948c766b796c756b6d6d6d6c547954797e886d6d6d6d6d6d0126790126790126790126790126790126790126790126790126790126790126790126796079013a79013a79013a79013a79013a79013a79013a7956797657795779577957795779577955766b796c756b6d6d6d6c7e7d7757795779577957795779577954766b796c756b6d6d6d6c7e7d7757795779577957795779577953766b796c756b6d6d6d6c7e7d7757795779577957795779577952766b796c756b6d6d6d6c7e7d7757795779577957795779577951766b796c756b6d6d6d6c7e7d7757795779577957795779577900766b796c756b6d6d6d6c7e7d77a96b6d6d6d6d6c5d795d795d795d795d795d795d795d795d795d795d795d795a795a798800597959795979597953766b796c756b6d6d6c567956795679567953766b796c756b6d6d6c768277005f799f637052797e53797e7e547a7572537a537975686d75597959795979597952766b796c756b6d6d6c567956795679567952766b796c756b6d6d6c768277515f799f637052797e53797e7e547a7572537a537975686d75597959795979597951766b796c756b6d6d6c567956795679567951766b796c756b6d6d6c768277525f799f637052797e53797e7e547a7572537a537975686d75597959795979597900766b796c756b6d6d6c567956795679567900766b796c756b6d6d6c768277535f799f637052797e53797e7e547a7572537a537975686d755c79787e52797eaa6b6d6d6d6d6d6d6c77527988765178a978557894000052799f637600a97e77685152799f637600a97e77685252799f637600a97e77685352799f637600a97e77685452799f637600a97e776877777ea9066a1863617401787e777777577957795779577954007600a26976549f69948c766b796c756b6d6d6c886d6d6d6d6d6d6d00000139766b796c787701317987916952790140766b796c75a97e537a757b7b52797877827700a0636e0137766b796c7578827d770122a1696e7e53797e7777777e7b757c687501398c766b796c7877013179879169527901408c766b796c75a97e537a757b7b52797877827700a0636e01378c766b796c7578827d770122a1696e7e53797e7777777e7b757c687501395294766b796c7877013179879169527901405294766b796c75a97e537a757b7b52797877827700a0636e01375294766b796c7578827d770122a1696e7e53797e7777777e7b757c687501395394766b796c7877013179879169527901405394766b796c75a97e537a757b7b52797877827700a0636e01375394766b796c7578827d770122a1696e7e53797e7777777e7b757c687501395494766b796c7877013179879169527901405494766b796c75a97e537a757b7b52797877827700a0636e01375494766b796c7578827d770122a1696e7e53797e7777777e7b757c687578a9066a1863617401787e770800000000000000007882777e787e776e7c7ea876011979876b6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6c77", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftClosedMinter.json b/packages/smartcontracts/artifacts/contracts/nft/nftClosedMinter.json new file mode 100644 index 00000000..fbe2ea51 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftClosedMinter.json @@ -0,0 +1,430 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftClosedMinter", + "md5": "fc2429e864deaa9b44d1e6f24282334f", + "structs": [ + { + "name": "NftClosedMinterState", + "params": [ + { + "name": "nftScript", + "type": "bytes" + }, + { + "name": "quotaMaxLocalId", + "type": "int" + }, + { + "name": "nextLocalId", + "type": "int" + } + ], + "genericTypes": [] + }, + { + "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": "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": "NftClosedMinterProto", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "SigHashUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "Backtrace", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "CAT721Proto", + "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": "NftClosedMinterState" + }, + { + "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": "../nftClosedMinter.scrypt", + "hex": "0800000000000000002079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984c807bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179842f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a03100000000005279567a757171557a78557a75547a547a547a547a76547a7572537a6d750126790126790126790126790126790126790126790126790126790126790126790126790126790126790111795e797e5d797e5c797e5b797e5a797e59797e58797e57797e56797e55797e54797e53797ea8011379787ea85279017f9f695279009c6301006752796878557952797e8801167955797e54798b7e6b6d6d6d6d6d6d6d6d6c775679ad011879011879011879011879011879011879011879011879011879011879012e790129795b795b795b795b795b795b790056766b796c756e7e777755766b796c756e7e777754766b796c756e7e777753766b796c756e7e777752766b796c756e7e777751766b796c756e7e7b756b6d6d6d6c77a852798855796e760087630100776876030000007e527987777777695479537978760087630100776876030000007e527987777777695b795b795b795b795b795b79565c797600a26976569f69948c766b796c756b6d6d6d6c547954797e886d6d6d6d6d6d5e795e795e795e795e795e790128795679567956795679567956790056766b796c756e827752797e7e777755766b796c756e827752797e7e777754766b796c756e827752797e7e777753766b796c756e827752797e7e777752766b796c756e827752797e7e777751766b796c756e827752797e7e7b756b6d6d6d6c77a878886d6d6d75015b79015b79015b79015b79015b79015b790164790164790164796f757e787ea9777777014c79014c79014c79014c79014c79014c7956007600a26976569f69948c766b796c756b6d6d6d6c0119795879066a1863617401787e77527988577957795779577957795d79007657766b796c75a97e7d7756766b796c75a97e7d7755766b796c75a97e7d7754766b796c75a97e7d7753766b796c75a97e7d77a95279876b6d6d6d6c77695279587958795879587958795557798c7600a26976559f69948c766b796c756b6d6d756c886d6d6d6d75011279009d5e0113797600a26976569f6994766b796c750111790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790157790132790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790116790116797e7601167901167901167901167901167901167955766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167954766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167953766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167952766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167951766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167900766b796c756b6d6d6d6c7e7d775f797e775f795f79885d795d795d795d795d795d7955766b796c756b6d6d6d6c58795879587958795879587955766b796c756b6d6d6d6c768277000113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7954766b796c756b6d6d6d6c58795879587958795879587954766b796c756b6d6d6d6c768277510113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7953766b796c756b6d6d6d6c58795879587958795879587953766b796c756b6d6d6d6c768277520113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7952766b796c756b6d6d6d6c58795879587958795879587952766b796c756b6d6d6d6c768277530113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7951766b796c756b6d6d6d6c58795879587958795879587951766b796c756b6d6d6d6c768277540113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7900766b796c756b6d6d6d6c58795879587958795879587900766b796c756b6d6d6d6c768277550113799f637052797e53797e7e547a7572537a537975686d787752797eaa6b6d6d6d6d6d6d6d6d6d6d6d6d6c88011979011979011979011979707e01007e787e6b6d6d6c012f79012f79012f79012f79012f79012f7956011d797600a26976569f69948c766b796c756b6d6d6d6c8801177901197978760087630100776876030000007e527987777777690119790119797e7653798764011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579012d79012c79011679011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579007601147901147901147901147953766b796c756b6d6d6c7e7d7701147901147901147901147952766b796c756b6d6d6c7e7d7701147901147901147901147951766b796c756b6d6d6c7e7d7701147901147901147901147900766b796c756b6d6d6c7e775f795f79885d795d795d795d795d795d7955766b796c756b6d6d6d6c58795879587958795879587955766b796c756b6d6d6d6c768277000113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7954766b796c756b6d6d6d6c58795879587958795879587954766b796c756b6d6d6d6c768277510113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7953766b796c756b6d6d6d6c58795879587958795879587953766b796c756b6d6d6d6c768277520113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7952766b796c756b6d6d6d6c58795879587958795879587952766b796c756b6d6d6d6c768277530113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7951766b796c756b6d6d6d6c58795879587958795879587951766b796c756b6d6d6d6c768277540113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7900766b796c756b6d6d6d6c58795879587958795879587900766b796c756b6d6d6d6c768277550113799f637052797e53797e7e547a7572537a537975686d787752797eaa6b6d6d6d6d6d6d6d6d6d6d6c5379885979597959795979597959795658797600a26976569f69948c766b796c756b6d6d6d6c78886d6d6d6d6d6d6d6d6d6d6d686d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d750000000160798b760163799f63547901667978827d770122a1696e7e53797e777777537a757b7b53790164790164790164798b6f757e787ea9777777a97e547a7572537a537975788b7b757c680169790162799d5379016b79016b7978827701149d6e7ea97777a97e547a7572537a53797501637901657978827d770122a1696e7e53797e77777752798b537a757b7b5279755479537901727901727901727901727901727956795679557894000052799f637600a97e77685152799f637600a97e77685252799f637600a97e77685352799f637600a97e77685452799f637600a97e776877777ea9557955795579557955795579007657766b796c75a97e7d7756766b796c75a97e7d7755766b796c75a97e7d7754766b796c75a97e7d7753766b796c75a97e7d77a95279876b6d6d6d6c776976066a1863617401787e770800000000000000007882777e787e6b6d6d6d6d6c775f795f7976607987646e78827d770122a1696e7e53797e77777767006877777856797e53797e787ea876012a79885b79016d79016d797ea988016a79016c79ac6b6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6c", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftClosedMinterProto.json b/packages/smartcontracts/artifacts/contracts/nft/nftClosedMinterProto.json new file mode 100644 index 00000000..aafd7ed7 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftClosedMinterProto.json @@ -0,0 +1,46 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftClosedMinterProto", + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "structs": [ + { + "name": "NftClosedMinterState", + "params": [ + { + "name": "nftScript", + "type": "bytes" + }, + { + "name": "quotaMaxLocalId", + "type": "int" + }, + { + "name": "nextLocalId", + "type": "int" + } + ], + "genericTypes": [] + } + ], + "library": [ + { + "name": "NftClosedMinterProto", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "constructor", + "params": [] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftClosedMinterProto.scrypt", + "hex": "", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftGuardProto.json b/packages/smartcontracts/artifacts/contracts/nft/nftGuardProto.json new file mode 100644 index 00000000..895f2b7c --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftGuardProto.json @@ -0,0 +1,42 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftGuardProto", + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "structs": [ + { + "name": "NftGuardConstState", + "params": [ + { + "name": "collectionScript", + "type": "bytes" + }, + { + "name": "localIdArray", + "type": "int[6]" + } + ], + "genericTypes": [] + } + ], + "library": [ + { + "name": "NftGuardProto", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "constructor", + "params": [] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftGuardProto.scrypt", + "hex": "", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinter.json b/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinter.json new file mode 100644 index 00000000..d1b79385 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinter.json @@ -0,0 +1,466 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftOpenMinter", + "md5": "86fe8c275b45c796bb9500b815d48820", + "structs": [ + { + "name": "NftOpenMinterState", + "params": [ + { + "name": "nftScript", + "type": "bytes" + }, + { + "name": "merkleRoot", + "type": "bytes" + }, + { + "name": "nextLocalId", + "type": "int" + } + ], + "genericTypes": [] + }, + { + "name": "NftMerkleLeaf", + "params": [ + { + "name": "commitScript", + "type": "bytes" + }, + { + "name": "localId", + "type": "int" + }, + { + "name": "isMined", + "type": "bool" + } + ], + "genericTypes": [] + }, + { + "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": "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": "NftOpenMinterProto", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "NftOpenMinterMerkleTree", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "SigHashUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "Backtrace", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "CAT721Proto", + "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": "neighbor", + "type": "bytes[15]" + }, + { + "name": "neighborType", + "type": "bool[15]" + }, + { + "name": "preminerPubKeyPrefix", + "type": "bytes" + }, + { + "name": "preminerPubKey", + "type": "PubKey" + }, + { + "name": "preminerSig", + "type": "Sig" + }, + { + "name": "minterSatoshis", + "type": "bytes" + }, + { + "name": "nftSatoshis", + "type": "bytes" + }, + { + "name": "preState", + "type": "NftOpenMinterState" + }, + { + "name": "preTxStatesInfo", + "type": "PreTxStatesInfo" + }, + { + "name": "backtraceInfo", + "type": "BacktraceInfo" + }, + { + "name": "shPreimage", + "type": "SHPreimage" + }, + { + "name": "prevoutsCtx", + "type": "PrevoutsCtx" + }, + { + "name": "spentScriptsCtx", + "type": "bytes[6]" + }, + { + "name": "changeInfo", + "type": "ChangeInfo" + } + ] + }, + { + "type": "constructor", + "params": [ + { + "name": "genesisOutpoint", + "type": "bytes" + }, + { + "name": "maxCount", + "type": "int" + }, + { + "name": "premine", + "type": "int" + }, + { + "name": "premineAddr", + "type": "bytes" + } + ] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftOpenMinter.scrypt", + "hex": "0800000000000000002079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984c807bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179842f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a0310000000000005379587a75577a577a577a577a577a577a577a5279577a75567a567a567a567a567a567a78567a757171557a76557a75547a547a547a547a6d6d0127790127790127790127790127790127790127790127790127790127790127790127790127790127790112795e797e5d797e5c797e5b797e5a797e59797e58797e57797e56797e55797e54797e53797ea8011479787ea85279017f9f695279009c6301006752796878557952797e8801177955797e54798b7e6b6d6d6d6d6d6d6d6d6c775779ad011979011979011979011979011979011979011979011979011979011979012f79012a795b795b795b795b795b795b790056766b796c756e7e777755766b796c756e7e777754766b796c756e7e777753766b796c756e7e777752766b796c756e7e777751766b796c756e7e7b756b6d6d6d6c77a852798855796e760087630100776876030000007e527987777777695479537978760087630100776876030000007e527987777777695b795b795b795b795b795b79565c797600a26976569f69948c766b796c756b6d6d6d6c547954797e886d6d6d6d6d6d5f795f795f795f795f795f790129795679567956795679567956790056766b796c756e827752797e7e777755766b796c756e827752797e7e777754766b796c756e827752797e7e777753766b796c756e827752797e7e777752766b796c756e827752797e7e777751766b796c756e827752797e7e7b756b6d6d6d6c77a878886d6d6d75015c79015c79015c79015c79015c79015c790165790165790165796f757e787ea9777777014d79014d79014d79014d79014d79014d7956007600a26976569f69948c766b796c756b6d6d6d6c011a795879066a1863617401787e77527988577957795779577957795d79007657766b796c75a97e7d7756766b796c75a97e7d7755766b796c75a97e7d7754766b796c75a97e7d7753766b796c75a97e7d77a95279876b6d6d6d6c77695279587958795879587958795557798c7600a26976559f69948c766b796c756b6d6d756c886d6d6d6d75011379009d5f0114797600a26976569f6994766b796c755f766b796c7877016079006f76635167010068707e787e6b6d6d6c5479016479516f76635167010068707e787e6b6d6d6c5479a978a9028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028e0079028800790120790120790111766b796c7563780121766b796c757ea97b757c760121766b796c757ea977670120766b796c7552797ea97b757c0120766b796c75787ea9776801118c766b796c75637801218c766b796c757ea97b757c7601218c766b796c757ea9776701208c766b796c7552797ea97b757c01208c766b796c75787ea9776801115294766b796c75637801215294766b796c757ea97b757c7601215294766b796c757ea9776701205294766b796c7552797ea97b757c01205294766b796c75787ea9776801115394766b796c75637801215394766b796c757ea97b757c7601215394766b796c757ea9776701205394766b796c7552797ea97b757c01205394766b796c75787ea9776801115494766b796c75637801215494766b796c757ea97b757c7601215494766b796c757ea9776701205494766b796c7552797ea97b757c01205494766b796c75787ea9776801115594766b796c75637801215594766b796c757ea97b757c7601215594766b796c757ea9776701205594766b796c7552797ea97b757c01205594766b796c75787ea9776801115694766b796c75637801215694766b796c757ea97b757c7601215694766b796c757ea9776701205694766b796c7552797ea97b757c01205694766b796c75787ea9776801115794766b796c75637801215794766b796c757ea97b757c7601215794766b796c757ea9776701205794766b796c7552797ea97b757c01205794766b796c75787ea9776801115894766b796c75637801215894766b796c757ea97b757c7601215894766b796c757ea9776701205894766b796c7552797ea97b757c01205894766b796c75787ea9776801115994766b796c75637801215994766b796c757ea97b757c7601215994766b796c757ea9776701205994766b796c7552797ea97b757c01205994766b796c75787ea9776801115a94766b796c75637801215a94766b796c757ea97b757c7601215a94766b796c757ea9776701205a94766b796c7552797ea97b757c01205a94766b796c75787ea9776801115b94766b796c75637801215b94766b796c757ea97b757c7601215b94766b796c757ea9776701205b94766b796c7552797ea97b757c01205b94766b796c75787ea9776801115c94766b796c75637801215c94766b796c757ea97b757c7601215c94766b796c757ea9776701205c94766b796c7552797ea97b757c01205c94766b796c75787ea9776801115d94766b796c75637801215d94766b796c757ea97b757c7601215d94766b796c757ea9776701205d94766b796c7552797ea97b757c01205d94766b796c75787ea97768785379886b6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6c011c79016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279016279013e79013b790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790131790116790116797e7601167901167901167901167901167901167955766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167954766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167953766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167952766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167951766b796c756b6d6d6d6c7e7d7701167901167901167901167901167901167900766b796c756b6d6d6d6c7e7d775f797e775f795f79885d795d795d795d795d795d7955766b796c756b6d6d6d6c58795879587958795879587955766b796c756b6d6d6d6c768277000113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7954766b796c756b6d6d6d6c58795879587958795879587954766b796c756b6d6d6d6c768277510113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7953766b796c756b6d6d6d6c58795879587958795879587953766b796c756b6d6d6d6c768277520113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7952766b796c756b6d6d6d6c58795879587958795879587952766b796c756b6d6d6d6c768277530113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7951766b796c756b6d6d6d6c58795879587958795879587951766b796c756b6d6d6d6c768277540113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7900766b796c756b6d6d6d6c58795879587958795879587900766b796c756b6d6d6d6c768277550113799f637052797e53797e7e547a7572537a537975686d787752797eaa6b6d6d6d6d6d6d6d6d6d6d6d6d6c88011979011979011979011979707e01007e787e6b6d6d6c012f79012f79012f79012f79012f79012f7956011d797600a26976569f69948c766b796c756b6d6d6d6c8801177901197978760087630100776876030000007e527987777777690119790119797e7653798764011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579012d79012c79011679011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579011579007601147901147901147901147953766b796c756b6d6d6c7e7d7701147901147901147901147952766b796c756b6d6d6c7e7d7701147901147901147901147951766b796c756b6d6d6c7e7d7701147901147901147901147900766b796c756b6d6d6c7e775f795f79885d795d795d795d795d795d7955766b796c756b6d6d6d6c58795879587958795879587955766b796c756b6d6d6d6c768277000113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7954766b796c756b6d6d6d6c58795879587958795879587954766b796c756b6d6d6d6c768277510113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7953766b796c756b6d6d6d6c58795879587958795879587953766b796c756b6d6d6d6c768277520113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7952766b796c756b6d6d6d6c58795879587958795879587952766b796c756b6d6d6d6c768277530113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7951766b796c756b6d6d6d6c58795879587958795879587951766b796c756b6d6d6d6c768277540113799f637052797e53797e7e547a7572537a537975686d755d795d795d795d795d795d7900766b796c756b6d6d6d6c58795879587958795879587900766b796c756b6d6d6d6c768277550113799f637052797e53797e7e547a7572537a537975686d787752797eaa6b6d6d6d6d6d6d6d6d6d6d6c5379885979597959795979597959795658797600a26976569f69948c766b796c756b6d6d6d6c78886d6d6d6d6d6d6d6d6d6d6d686d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d75000051016b798b760112799e6353795f7901727978827d770122a1696e7e53797e7777777e547a7572537a5379755279016f79567953796f757e787ea9777777a97e537a757b7b527975788b7b757c6802920079016d799d5279029400790294007978827701149d6e7ea97777a97e537a757b7b527975016e7901707978827d770122a1696e7e53797e777777029300790112799f630174790174797ea901117988017279017479ad6870029b0079029b0079029b0079029b0079029b007956795679557894000052799f637600a97e77685152799f637600a97e77685252799f637600a97e77685352799f637600a97e77685452799f637600a97e776877777ea9557955795579557955795579007657766b796c75a97e7d7756766b796c75a97e7d7755766b796c75a97e7d7754766b796c75a97e7d7753766b796c75a97e7d77a95279876b6d6d6d6c776976066a1863617401787e770800000000000000007882777e787e6b6d6d6d6d6c77011a79011a7976011b7987646e78827d770122a1696e7e53797e77777767006877777857797e53797e787ea876013579876b6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6c77", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinterMerkleTree.json b/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinterMerkleTree.json new file mode 100644 index 00000000..258334d4 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinterMerkleTree.json @@ -0,0 +1,27 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftOpenMinterMerkleTree", + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "structs": [], + "library": [ + { + "name": "NftOpenMinterMerkleTree", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "constructor", + "params": [] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftOpenMinterMerkleTree.scrypt", + "hex": "", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinterProto.json b/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinterProto.json new file mode 100644 index 00000000..4dd21383 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftOpenMinterProto.json @@ -0,0 +1,64 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftOpenMinterProto", + "md5": "d41d8cd98f00b204e9800998ecf8427e", + "structs": [ + { + "name": "NftOpenMinterState", + "params": [ + { + "name": "nftScript", + "type": "bytes" + }, + { + "name": "merkleRoot", + "type": "bytes" + }, + { + "name": "nextLocalId", + "type": "int" + } + ], + "genericTypes": [] + }, + { + "name": "NftMerkleLeaf", + "params": [ + { + "name": "commitScript", + "type": "bytes" + }, + { + "name": "localId", + "type": "int" + }, + { + "name": "isMined", + "type": "bool" + } + ], + "genericTypes": [] + } + ], + "library": [ + { + "name": "NftOpenMinterProto", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "constructor", + "params": [] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftOpenMinterProto.scrypt", + "hex": "", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/artifacts/contracts/nft/nftTransferGuard.json b/packages/smartcontracts/artifacts/contracts/nft/nftTransferGuard.json new file mode 100644 index 00000000..0c69f257 --- /dev/null +++ b/packages/smartcontracts/artifacts/contracts/nft/nftTransferGuard.json @@ -0,0 +1,353 @@ +{ + "version": 9, + "compilerVersion": "1.19.4+commit.cfee948", + "contract": "NftTransferGuard", + "md5": "477458c3bb4a3b586664dddf525e5060", + "structs": [ + { + "name": "NftGuardConstState", + "params": [ + { + "name": "collectionScript", + "type": "bytes" + }, + { + "name": "localIdArray", + "type": "int[6]" + } + ], + "genericTypes": [] + }, + { + "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": "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": "NftGuardProto", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "SigHashUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "StateUtils", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "TxProof", + "params": [], + "properties": [], + "genericTypes": [] + }, + { + "name": "TxUtil", + "params": [], + "properties": [], + "genericTypes": [] + } + ], + "alias": [], + "abi": [ + { + "type": "function", + "name": "transfer", + "index": 0, + "params": [ + { + "name": "curTxoStateHashes", + "type": "bytes[5]" + }, + { + "name": "ownerAddrOrScriptList", + "type": "bytes[5]" + }, + { + "name": "localIdList", + "type": "int[5]" + }, + { + "name": "nftOutputMaskList", + "type": "bool[5]" + }, + { + "name": "outputSatoshisList", + "type": "bytes[5]" + }, + { + "name": "nftSatoshis", + "type": "bytes" + }, + { + "name": "preState", + "type": "NftGuardConstState" + }, + { + "name": "preTx", + "type": "XrayedTxIdPreimg3" + }, + { + "name": "shPreimage", + "type": "SHPreimage" + }, + { + "name": "prevoutsCtx", + "type": "PrevoutsCtx" + }, + { + "name": "spentScripts", + "type": "bytes[6]" + } + ] + }, + { + "type": "constructor", + "params": [] + } + ], + "stateProps": [], + "buildType": "debug", + "file": "../nftTransferGuard.scrypt", + "hex": "2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984c807bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179842f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a03100000120790120790120790120790120790120790120790120790120790120790120790120790120790120795e795e797e5d797e5c797e5b797e5a797e59797e58797e57797e56797e55797e54797e53797ea86079787ea85279017f9f695279009c6301006752796878557952797e8801137955797e54798b7e6b6d6d6d6d6d6d6d6d6c775379ad0112790112790112790112790112790112790112790112790112790112790128790123795b795b795b795b795b795b790056766b796c756e7e777755766b796c756e7e777754766b796c756e7e777753766b796c756e7e777752766b796c756e7e777751766b796c756e7e7b756b6d6d6d6c77a852798855796e760087630100776876030000007e527987777777695479537978760087630100776876030000007e527987777777695b795b795b795b795b795b79565c797600a26976569f69948c766b796c756b6d6d6d6c547954797e886d6d6d6d6d6d5879587958795879587958790122795679567956795679567956790056766b796c756e827752797e7e777755766b796c756e827752797e7e777754766b796c756e827752797e7e777753766b796c756e827752797e7e777752766b796c756e827752797e7e777751766b796c756e827752797e7e7b756b6d6d6d6c77a878886d6d6d75012c79012c79012c79012c79012c79012c79012c79012c79012c79012c79012c79012c7901167901407901407901407901407901407901407901407956797657795779577957795779577955766b796c756b6d6d6d6c7e7d7757795779577957795779577954766b796c756b6d6d6d6c7e7d7757795779577957795779577953766b796c756b6d6d6d6c7e7d7757795779577957795779577952766b796c756b6d6d6d6c7e7d7757795779577957795779577951766b796c756b6d6d6d6c7e7d7757795779577957795779577900766b796c756b6d6d6d6c7e7d77a96b6d6d6d6d6c5d795d795d795d795d795d795d795d795d795d795d795d795a795a798800597959795979597953766b796c756b6d6d6c567956795679567953766b796c756b6d6d6c768277005f799f637052797e53797e7e547a7572537a537975686d75597959795979597952766b796c756b6d6d6c567956795679567952766b796c756b6d6d6c768277515f799f637052797e53797e7e547a7572537a537975686d75597959795979597951766b796c756b6d6d6c567956795679567951766b796c756b6d6d6c768277525f799f637052797e53797e7e547a7572537a537975686d75597959795979597900766b796c756b6d6d6c567956795679567900766b796c756b6d6d6c768277535f799f637052797e53797e7e547a7572537a537975686d755c79787e52797eaa6b6d6d6d6d6d6d6c77527988765178a978557894000052799f637600a97e77685152799f637600a97e77685252799f637600a97e77685352799f637600a97e77685452799f637600a97e776877777ea9066a1863617401787e777777577957795779577954007600a26976549f69948c766b796c756b6d6d6c886d6d6d6d6d6d6d4f766e76005e766b796c7877013b79876301397901397901397901397901397901397955766b796c756b6d6d6d6c52797600a26976559f69587a78008763757868587a527951876375527968587a537952876375537968587a547953876375547968587a557954876375557968557a75577a577a577a75788b7b757c68755d766b796c7877013b79876301397901397901397901397901397901397954766b796c756b6d6d6d6c52797600a26976559f69587a78008763757868587a527951876375527968587a537952876375537968587a547953876375547968587a557954876375557968557a75577a577a577a75788b7b757c68755c766b796c7877013b79876301397901397901397901397901397901397953766b796c756b6d6d6d6c52797600a26976559f69587a78008763757868587a527951876375527968587a537952876375537968587a547953876375547968587a557954876375557968557a75577a577a577a75788b7b757c68755b766b796c7877013b79876301397901397901397901397901397901397952766b796c756b6d6d6d6c52797600a26976559f69587a78008763757868587a527951876375527968587a537952876375537968587a547953876375547968587a557954876375557968557a75577a577a577a75788b7b757c68755a766b796c7877013b79876301397901397901397901397901397901397951766b796c756b6d6d6d6c52797600a26976559f69587a78008763757868587a527951876375527968587a537952876375537968587a547953876375547968587a557954876375557968557a75577a577a577a75788b7b757c687559766b796c7877013b79876301397901397901397901397901397901397900766b796c756b6d6d6d6c52797600a26976559f69587a78008763757868587a527951876375527968587a537952876375537968587a547953876375547968587a557954876375557968557a75577a577a577a75788b7b757c6875000000013c79013e7978827d770122a1696e7e53797e7777770152766b796c750149766b796c75635a53797600a26976559f6994766b796c75547953797e557a75547a547a547a547a6e7ea9a9015a766b796c75a978887800a2690150766b796c7552799d5679787e577a75567a567a567a567a567a567a56797554798b557a75547a547a547a547a54796d756776013f7987916954790159766b796c75a97e557a75547a547a547a547a54797877827700a0635379780146766b796c7578827d770122a1696e7e53797e7777777e547a7572537a53797568687501528c766b796c7501498c766b796c75635a53797600a26976559f6994766b796c75547953797e557a75547a547a547a547a6e7ea9a9015a8c766b796c75a978887800a26901508c766b796c7552799d5679787e577a75567a567a567a567a567a567a56797554798b557a75547a547a547a547a54796d756776013f79879169547901598c766b796c75a97e557a75547a547a547a547a54797877827700a06353797801468c766b796c7578827d770122a1696e7e53797e7777777e547a7572537a53797568687501525294766b796c7501495294766b796c75635a53797600a26976559f6994766b796c75547953797e557a75547a547a547a547a6e7ea9a9015a5294766b796c75a978887800a26901505294766b796c7552799d5679787e577a75567a567a567a567a567a567a56797554798b557a75547a547a547a547a54796d756776013f79879169547901595294766b796c75a97e557a75547a547a547a547a54797877827700a06353797801465294766b796c7578827d770122a1696e7e53797e7777777e547a7572537a53797568687501525394766b796c7501495394766b796c75635a53797600a26976559f6994766b796c75547953797e557a75547a547a547a547a6e7ea9a9015a5394766b796c75a978887800a26901505394766b796c7552799d5679787e577a75567a567a567a567a567a567a56797554798b557a75547a547a547a547a54796d756776013f79879169547901595394766b796c75a97e557a75547a547a547a547a54797877827700a06353797801465394766b796c7578827d770122a1696e7e53797e7777777e547a7572537a53797568687501525494766b796c7501495494766b796c75635a53797600a26976559f6994766b796c75547953797e557a75547a547a547a547a6e7ea9a9015a5494766b796c75a978887800a26901505494766b796c7552799d5679787e577a75567a567a567a567a567a567a56797554798b557a75547a547a547a547a54796d756776013f79879169547901595494766b796c75a97e557a75547a547a547a547a54797877827700a06353797801465494766b796c7578827d770122a1696e7e53797e7777777e547a7572537a537975686875547952799d5379a9066a1863617401787e770800000000000000007882777e787e777654797ea876012779876b6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6c", + "sourceMapFile": "" +} \ No newline at end of file diff --git a/packages/smartcontracts/package.json b/packages/smartcontracts/package.json index d3c005f4..79aea5c8 100644 --- a/packages/smartcontracts/package.json +++ b/packages/smartcontracts/package.json @@ -1,6 +1,6 @@ { "name": "@cat-protocol/cat-smartcontracts", - "version": "0.1.2", + "version": "0.2.1", "description": "CAT protocol smart contracts.", "author": "", "main": "./dist/index.js", @@ -30,18 +30,18 @@ ] }, "dependencies": { + "@cmdcode/buff": "^2.2.4", + "@cmdcode/crypto-tools": "^2.7.4", + "@cmdcode/tapscript": "^1.4.4", + "bigi": "^1.4.2", + "bitcore-lib-inquisition": "^10.0.30", "cbor": "^9.0.2", "dotenv": "^16.0.3", - "scrypt-ts": "latest", - "varuint-bitcoin": "=1.1.2", "ecpair": "^2.1.0", "ecurve": "^1.0.6", - "bitcore-lib-inquisition": "^10.0.30", "js-sha256": "^0.9.0", - "bigi": "^1.4.2", - "@cmdcode/buff": "^2.2.4", - "@cmdcode/crypto-tools": "^2.7.4", - "@cmdcode/tapscript": "^1.4.4" + "scrypt-ts": "1.4.0", + "varuint-bitcoin": "=1.1.2" }, "devDependencies": { "@types/chai": "^4.3.4", diff --git a/packages/smartcontracts/src/contracts/nft/cat721.ts b/packages/smartcontracts/src/contracts/nft/cat721.ts new file mode 100644 index 00000000..688b0749 --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/cat721.ts @@ -0,0 +1,174 @@ +import { + ByteString, + SmartContract, + prop, + method, + assert, + PubKey, + Sig, + hash160, + FixedArray, +} from 'scrypt-ts' +import { + PrevoutsCtx, + SHPreimage, + SigHashUtils, + SpentScriptsCtx, +} from '../utils/sigHashUtils' +import { MAX_INPUT, STATE_OUTPUT_INDEX, int32 } from '../utils/txUtil' +import { TxProof, XrayedTxIdPreimg3 } from '../utils/txProof' +import { PreTxStatesInfo, StateUtils } from '../utils/stateUtils' +import { CAT721State, CAT721Proto } from './cat721Proto' +import { NftGuardConstState, NftGuardProto } from './nftGuardProto' +import { Backtrace, BacktraceInfo } from '../utils/backtrace' + +export type NftGuardInfo = { + tx: XrayedTxIdPreimg3 + inputIndexVal: int32 + outputIndex: ByteString + guardState: NftGuardConstState +} + +export type NftUnlockArgs = { + // `true`: spend by user, `false`: spend by contract + isUserSpend: boolean + + // user spend args + userPubKeyPrefix: ByteString + userPubKey: PubKey + userSig: Sig + + // contract spend arg + contractInputIndex: int32 +} + +export class CAT721 extends SmartContract { + @prop() + minterScript: ByteString + + @prop() + guardScript: ByteString + + constructor(minterScript: ByteString, guardScript: ByteString) { + super(...arguments) + this.minterScript = minterScript + this.guardScript = guardScript + } + + @method() + public unlock( + nftUnlockArgs: NftUnlockArgs, + + // verify preTx data part + preState: CAT721State, + preTxStatesInfo: PreTxStatesInfo, + + // amount check guard + guardInfo: NftGuardInfo, + // backtrace + backtraceInfo: BacktraceInfo, + // common args + // current tx info + shPreimage: SHPreimage, + prevoutsCtx: PrevoutsCtx, + spentScriptsCtx: SpentScriptsCtx + ) { + // Check sighash preimage. + assert( + this.checkSig( + SigHashUtils.checkSHPreimage(shPreimage), + SigHashUtils.Gx + ), + 'preimage check error' + ) + // check ctx + SigHashUtils.checkPrevoutsCtx( + prevoutsCtx, + shPreimage.hashPrevouts, + shPreimage.inputIndex + ) + SigHashUtils.checkSpentScriptsCtx( + spentScriptsCtx, + shPreimage.hashSpentScripts + ) + // verify state + StateUtils.verifyPreStateHash( + preTxStatesInfo, + CAT721Proto.stateHash(preState), + backtraceInfo.preTx.outputScriptList[STATE_OUTPUT_INDEX], + prevoutsCtx.outputIndexVal + ) + + const preScript = spentScriptsCtx[Number(prevoutsCtx.inputIndexVal)] + Backtrace.verifyToken( + prevoutsCtx.spentTxhash, + backtraceInfo, + this.minterScript, + preScript + ) + + // make sure the token is spent with a valid guard + this.valitateNftGuard( + guardInfo, + preScript, + preState, + prevoutsCtx.inputIndexVal, + prevoutsCtx.prevouts, + spentScriptsCtx + ) + + if (nftUnlockArgs.isUserSpend) { + // unlock token owned by user key + assert( + hash160( + nftUnlockArgs.userPubKeyPrefix + nftUnlockArgs.userPubKey + ) == preState.ownerAddr + ) + assert( + this.checkSig(nftUnlockArgs.userSig, nftUnlockArgs.userPubKey) + ) + } else { + // unlock token owned by contract script + assert( + preState.ownerAddr == + hash160( + spentScriptsCtx[ + Number(nftUnlockArgs.contractInputIndex) + ] + ) + ) + } + } + + @method() + valitateNftGuard( + guardInfo: NftGuardInfo, + preScript: ByteString, + preState: CAT721State, + inputIndexVal: int32, + prevouts: FixedArray, + spentScripts: SpentScriptsCtx + ): boolean { + // check amount script + const guardHashRoot = NftGuardProto.stateHash(guardInfo.guardState) + assert( + StateUtils.getStateScript(guardHashRoot, 1n) == + guardInfo.tx.outputScriptList[STATE_OUTPUT_INDEX] + ) + assert(guardInfo.guardState.collectionScript == preScript) + assert(preState.localId >= 0n) + assert( + guardInfo.guardState.localIdArray[Number(inputIndexVal)] == + preState.localId + ) + const guardTxid = TxProof.getTxIdFromPreimg3(guardInfo.tx) + assert( + prevouts[Number(guardInfo.inputIndexVal)] == + guardTxid + guardInfo.outputIndex + ) + assert( + spentScripts[Number(guardInfo.inputIndexVal)] == this.guardScript + ) + return true + } +} diff --git a/packages/smartcontracts/src/contracts/nft/cat721Proto.ts b/packages/smartcontracts/src/contracts/nft/cat721Proto.ts new file mode 100644 index 00000000..3e1973b3 --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/cat721Proto.ts @@ -0,0 +1,36 @@ +import { + assert, + ByteString, + hash160, + int2ByteString, + len, + method, + SmartContractLib, +} from 'scrypt-ts' +import { ADDRESS_HASH_LEN, int32 } from '../utils/txUtil' + +export type CAT721State = { + // owner(user/contract) address + ownerAddr: ByteString + // token index + localId: int32 +} + +export class CAT721Proto extends SmartContractLib { + @method() + static stateHash(_state: CAT721State): ByteString { + assert(len(_state.ownerAddr) == ADDRESS_HASH_LEN) + return hash160(_state.ownerAddr + int2ByteString(_state.localId)) + } + + static create(address: ByteString, localId: int32): CAT721State { + return { + ownerAddr: address, + localId: localId, + } + } + + static toByteString(tokenInfo: CAT721State) { + return tokenInfo.ownerAddr + int2ByteString(tokenInfo.localId) + } +} diff --git a/packages/smartcontracts/src/contracts/nft/nftBurnGuard.ts b/packages/smartcontracts/src/contracts/nft/nftBurnGuard.ts new file mode 100644 index 00000000..b03496e8 --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftBurnGuard.ts @@ -0,0 +1,71 @@ +import { + ByteString, + FixedArray, + SmartContract, + assert, + hash160, + len, + method, + sha256, + toByteString, +} from 'scrypt-ts' +import { PrevoutsCtx, SHPreimage, SigHashUtils } from '../utils/sigHashUtils' +import { MAX_STATE, MAX_TOKEN_OUTPUT, TxUtil } from '../utils/txUtil' +import { XrayedTxIdPreimg3 } from '../utils/txProof' +import { NftGuardConstState, NftGuardProto } from './nftGuardProto' +import { StateUtils, TxoStateHashes } from '../utils/stateUtils' + +export class NftBurnGuard extends SmartContract { + @method() + public burn( + curTxoStateHashes: TxoStateHashes, + outputScriptList: FixedArray, + outputSatoshisList: FixedArray, + // verify preTx data part + preState: NftGuardConstState, + // check deploy tx + preTx: XrayedTxIdPreimg3, + // + shPreimage: SHPreimage, + prevoutsCtx: PrevoutsCtx + ) { + // Check sighash preimage. + assert( + this.checkSig( + SigHashUtils.checkSHPreimage(shPreimage), + SigHashUtils.Gx + ), + 'preimage check error' + ) + SigHashUtils.checkPrevoutsCtx( + prevoutsCtx, + shPreimage.hashPrevouts, + shPreimage.inputIndex + ) + // check preTx + StateUtils.verifyGuardStateHash( + preTx, + prevoutsCtx.spentTxhash, + NftGuardProto.stateHash(preState) + ) + let stateHashString = toByteString('') + let outputs = toByteString('') + for (let i = 0; i < MAX_STATE; i++) { + const outputScript = outputScriptList[i] + // output note equal token locking script + assert(outputScript != preState.collectionScript) + stateHashString += hash160(curTxoStateHashes[i]) + if (len(outputScript) > 0) { + outputs += TxUtil.buildOutput( + outputScript, + outputSatoshisList[i] + ) + } + } + const stateOutput = TxUtil.buildOpReturnRoot( + TxUtil.getStateScript(hash160(stateHashString)) + ) + const hashOutputs = sha256(stateOutput + outputs) + assert(hashOutputs == shPreimage.hashOutputs, 'hashOutputs mismatch') + } +} diff --git a/packages/smartcontracts/src/contracts/nft/nftClosedMinter.ts b/packages/smartcontracts/src/contracts/nft/nftClosedMinter.ts new file mode 100644 index 00000000..35aa8a4b --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftClosedMinter.ts @@ -0,0 +1,148 @@ +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 { + NftClosedMinterState, + NftClosedMinterProto, +} from './nftClosedMinterProto' +import { + PreTxStatesInfo, + StateUtils, + TxoStateHashes, +} from '../utils/stateUtils' +import { CAT721Proto, CAT721State } from './cat721Proto' + +export class NftClosedMinter 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: NftClosedMinterState, + 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, + NftClosedMinterProto.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 + ) + let hashString = toByteString('') + let minterOutput = toByteString('') + let stateNumber = 0n + const nextLocalId = preState.nextLocalId + 1n + if (nextLocalId < preState.quotaMaxLocalId) { + minterOutput = TxUtil.buildOutput(preScript, minterSatoshis) + hashString += hash160( + NftClosedMinterProto.stateHash({ + nftScript: preState.nftScript, + quotaMaxLocalId: preState.quotaMaxLocalId, + nextLocalId: preState.nextLocalId + 1n, + }) + ) + stateNumber += 1n + } + assert(nftMint.localId == preState.nextLocalId) + hashString += hash160(CAT721Proto.stateHash(nftMint)) + const nft = TxUtil.buildOutput(preState.nftScript, nftSatoshis) + stateNumber += 1n + const stateOutput = StateUtils.getCurrentStateOutput( + hashString, + stateNumber, + curTxoStateHashes + ) + const changeOutput = TxUtil.getChangeOutput(changeInfo) + const hashOutputs = sha256( + stateOutput + minterOutput + nft + 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/nftClosedMinterProto.ts b/packages/smartcontracts/src/contracts/nft/nftClosedMinterProto.ts new file mode 100644 index 00000000..7a10d282 --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftClosedMinterProto.ts @@ -0,0 +1,45 @@ +import { + ByteString, + hash160, + int2ByteString, + method, + SmartContractLib, +} from 'scrypt-ts' +import { int32 } from '../utils/txUtil' + +export type NftClosedMinterState = { + nftScript: ByteString + quotaMaxLocalId: int32 + nextLocalId: int32 +} + +export class NftClosedMinterProto extends SmartContractLib { + @method() + static stateHash(_state: NftClosedMinterState): ByteString { + return hash160( + _state.nftScript + + int2ByteString(_state.quotaMaxLocalId) + + int2ByteString(_state.nextLocalId) + ) + } + + static create( + nftScript: ByteString, + quotaLocalId: int32, + nextLocalId: int32 + ): NftClosedMinterState { + return { + nftScript: nftScript, + quotaMaxLocalId: quotaLocalId, + nextLocalId: nextLocalId, + } + } + + static toByteString(closeMinterInfo: NftClosedMinterState) { + return ( + closeMinterInfo.nftScript + + int2ByteString(closeMinterInfo.quotaMaxLocalId) + + int2ByteString(closeMinterInfo.nextLocalId) + ) + } +} diff --git a/packages/smartcontracts/src/contracts/nft/nftGuardProto.ts b/packages/smartcontracts/src/contracts/nft/nftGuardProto.ts new file mode 100644 index 00000000..ac855e0c --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftGuardProto.ts @@ -0,0 +1,46 @@ +import { + ByteString, + FixedArray, + SmartContractLib, + hash160, + int2ByteString, + method, + toByteString, +} from 'scrypt-ts' +import { emptyBigIntArray, intArrayToByteString } from '../../lib/proof' +import { MAX_INPUT, int32 } from '../utils/txUtil' + +export type NftGuardConstState = { + collectionScript: ByteString + localIdArray: FixedArray +} + +export class NftGuardProto extends SmartContractLib { + @method() + static stateHash(_state: NftGuardConstState): ByteString { + let inputOutpointAll = _state.collectionScript + for (let i = 0; i < MAX_INPUT; i++) { + inputOutpointAll += int2ByteString(_state.localIdArray[i]) + } + return hash160(inputOutpointAll) + } + + static toByteString(state: NftGuardConstState) { + return NftGuardProto.toList(state).join('') + } + + static createEmptyState(): NftGuardConstState { + return { + collectionScript: toByteString(''), + localIdArray: emptyBigIntArray(), + } + } + + static toList(state: NftGuardConstState) { + const dataList = [ + state.collectionScript, + ...intArrayToByteString(state.localIdArray), + ] + return dataList + } +} diff --git a/packages/smartcontracts/src/contracts/nft/nftOpenMinter.ts b/packages/smartcontracts/src/contracts/nft/nftOpenMinter.ts new file mode 100644 index 00000000..d41bdeb1 --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftOpenMinter.ts @@ -0,0 +1,192 @@ +import { + method, + SmartContract, + assert, + prop, + ByteString, + PubKey, + Sig, + toByteString, + hash160, + sha256, +} from 'scrypt-ts' +import { ChangeInfo, STATE_OUTPUT_INDEX, TxUtil, int32 } from '../utils/txUtil' +import { + PrevoutsCtx, + SHPreimage, + SigHashUtils, + SpentScriptsCtx, +} from '../utils/sigHashUtils' +import { Backtrace, BacktraceInfo } from '../utils/backtrace' +import { + StateUtils, + PreTxStatesInfo, + TxoStateHashes, +} from '../utils/stateUtils' +import { CAT721Proto, CAT721State } from './cat721Proto' +import { + NftMerkleLeaf, + NftOpenMinterProto, + NftOpenMinterState, +} from './nftOpenMinterProto' +import { + LeafNeighbor, + LeafNeighborType, + NftOpenMinterMerkleTree, +} from './nftOpenMinterMerkleTree' + +export class NftOpenMinter extends SmartContract { + @prop() + genesisOutpoint: ByteString + + @prop() + max: int32 + + @prop() + premine: int32 + + @prop() + premineAddr: ByteString + + constructor( + genesisOutpoint: ByteString, + maxCount: int32, + premine: int32, + premineAddr: ByteString + ) { + super(...arguments) + this.genesisOutpoint = genesisOutpoint + this.max = maxCount + this.premine = premine + this.premineAddr = premineAddr + } + + @method() + public mint( + // + curTxoStateHashes: TxoStateHashes, + // contract logic args + nftMint: CAT721State, + + neighbor: LeafNeighbor, + neighborType: LeafNeighborType, + + // premine related args + preminerPubKeyPrefix: ByteString, + preminerPubKey: PubKey, + preminerSig: Sig, + + // satoshis locked in minter utxo + minterSatoshis: ByteString, + // satoshis locked in token utxo + nftSatoshis: ByteString, + // unlock utxo state info + preState: NftOpenMinterState, + preTxStatesInfo: PreTxStatesInfo, + // backtrace info, use b2g + backtraceInfo: BacktraceInfo, + // common args + // current tx info + shPreimage: SHPreimage, + prevoutsCtx: PrevoutsCtx, + spentScriptsCtx: 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( + spentScriptsCtx, + shPreimage.hashSpentScripts + ) + // verify state + StateUtils.verifyPreStateHash( + preTxStatesInfo, + NftOpenMinterProto.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 = spentScriptsCtx[Number(prevoutsCtx.inputIndexVal)] + // + const commitScript = spentScriptsCtx[1] + const oldLeaf: NftMerkleLeaf = { + commitScript: commitScript, + localId: preState.nextLocalId, + isMined: false, + } + const oldLeafBytes = NftOpenMinterProto.nftMerkleLeafToString(oldLeaf) + const newLeaf: NftMerkleLeaf = { + commitScript: commitScript, + localId: preState.nextLocalId, + isMined: true, + } + const newLeafBytes = NftOpenMinterProto.nftMerkleLeafToString(newLeaf) + const newMerkleRoot = NftOpenMinterMerkleTree.updateLeaf( + hash160(oldLeafBytes), + hash160(newLeafBytes), + neighbor, + neighborType, + preState.merkleRoot + ) + // back to genesis + Backtrace.verifyUnique( + prevoutsCtx.spentTxhash, + backtraceInfo, + this.genesisOutpoint, + preScript + ) + + let nftOpenMinterOutput = toByteString('') + let curStateHashes = toByteString('') + let curStateCnt = 1n + const nextLocalId = preState.nextLocalId + 1n + if (nextLocalId != this.max) { + nftOpenMinterOutput += TxUtil.buildOutput(preScript, minterSatoshis) + curStateHashes += hash160( + NftOpenMinterProto.stateHash({ + nftScript: preState.nftScript, + merkleRoot: newMerkleRoot, + nextLocalId: nextLocalId, + }) + ) + curStateCnt += 1n + } + assert(nftMint.localId == preState.nextLocalId) + // mint nft + curStateHashes += hash160(CAT721Proto.stateHash(nftMint)) + const nftOutput = TxUtil.buildOutput(preState.nftScript, nftSatoshis) + if (nftMint.localId < this.premine) { + // premine need checksig + assert( + hash160(preminerPubKeyPrefix + preminerPubKey) == + this.premineAddr + ) + assert(this.checkSig(preminerSig, preminerPubKey)) + } + const stateOutput = StateUtils.getCurrentStateOutput( + curStateHashes, + curStateCnt, + curTxoStateHashes + ) + const changeOutput = TxUtil.getChangeOutput(changeInfo) + const hashOutputs = sha256( + stateOutput + nftOpenMinterOutput + nftOutput + changeOutput + ) + assert(hashOutputs == shPreimage.hashOutputs, 'hashOutputs mismatch') + } +} diff --git a/packages/smartcontracts/src/contracts/nft/nftOpenMinterMerkleTree.ts b/packages/smartcontracts/src/contracts/nft/nftOpenMinterMerkleTree.ts new file mode 100644 index 00000000..009b9bca --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftOpenMinterMerkleTree.ts @@ -0,0 +1,59 @@ +import { + assert, + method, + ByteString, + hash160, + FixedArray, + SmartContractLib, +} from 'scrypt-ts' + +export const HEIGHT = 15 + +export type LeafNeighbor = FixedArray +export type LeafNeighborType = FixedArray + +export class NftOpenMinterMerkleTree extends SmartContractLib { + @method() + static updateLeaf( + oldLeaf: ByteString, + newLeaf: ByteString, + neighbor: LeafNeighbor, + neighborType: LeafNeighborType, + oldMerkleRoot: ByteString + ): ByteString { + let oldMerkleValue = oldLeaf + let newMerkleValue = newLeaf + for (let i = 0; i < HEIGHT - 1; i++) { + if (neighborType[i]) { + oldMerkleValue = hash160(oldMerkleValue + neighbor[i]) + newMerkleValue = hash160(newMerkleValue + neighbor[i]) + } else { + oldMerkleValue = hash160(neighbor[i] + oldMerkleValue) + newMerkleValue = hash160(neighbor[i] + newMerkleValue) + } + } + + assert(oldMerkleValue == oldMerkleRoot, 'oldMerkleValue illegal') + return newMerkleValue + } + + @method() + static verifyLeaf( + leaf: ByteString, + neighbor: FixedArray, + neighborType: FixedArray, + merkleRoot: ByteString + ): boolean { + let merkleValue = leaf + for (let i = 0; i < HEIGHT - 1; i++) { + if (neighborType[i]) { + merkleValue = hash160(merkleValue + neighbor[i]) + } else { + merkleValue = hash160(neighbor[i] + merkleValue) + } + } + + assert(merkleValue == merkleRoot, 'merkleValue illegal') + return true + } +} diff --git a/packages/smartcontracts/src/contracts/nft/nftOpenMinterProto.ts b/packages/smartcontracts/src/contracts/nft/nftOpenMinterProto.ts new file mode 100644 index 00000000..7ec2db98 --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftOpenMinterProto.ts @@ -0,0 +1,271 @@ +import { + ByteString, + hash160, + int2ByteString, + method, + SmartContractLib, + toByteString, +} from 'scrypt-ts' +import { int32 } from '../utils/txUtil' + +const LEFT_FLAG = true +const RIGHT_FLAG = false + +export type NftOpenMinterState = { + // mint nft script + nftScript: ByteString + // init merkle root + merkleRoot: ByteString + // next mint local id + nextLocalId: int32 +} + +export type NftMerkleLeaf = { + // commit script + commitScript: ByteString + // init merkle root + localId: int32 + // flag is mined + isMined: boolean +} + +export class NftOpenMinterProto extends SmartContractLib { + @method() + static stateHash(_state: NftOpenMinterState): ByteString { + return hash160( + _state.nftScript + + _state.merkleRoot + + int2ByteString(_state.nextLocalId) + ) + } + + @method() + static nftMerkleLeafToString(leaf: NftMerkleLeaf): ByteString { + const isMinedByte = leaf.isMined + ? toByteString('01') + : toByteString('00') + return leaf.commitScript + int2ByteString(leaf.localId) + isMinedByte + } + + static create( + tokenScript: ByteString, + merkleRoot: ByteString, + mintNumber: bigint + ): NftOpenMinterState { + return { + nftScript: tokenScript, + merkleRoot: merkleRoot, + nextLocalId: mintNumber, + } + } + + static toByteString(_state: NftOpenMinterState) { + return ( + _state.nftScript + + _state.merkleRoot + + int2ByteString(_state.nextLocalId) + ) + } +} + +export class NftOpenMinterMerkleTreeData { + leafArray: NftMerkleLeaf[] = [] + height: number + emptyHashs: string[] = [] + hashNodes: string[][] = [] + maxLeafSize: number + + constructor(leafArray: NftMerkleLeaf[], height: number) { + this.height = height + this.maxLeafSize = Math.pow(2, this.height - 1) + this.leafArray = leafArray + const emptyHash = hash160('') + this.emptyHashs.push(emptyHash) + for (let i = 1; i < height; i++) { + const prevHash = this.emptyHashs[i - 1] + this.emptyHashs[i] = hash160(prevHash + prevHash) + } + + this.buildMerkleTree() + } + + getLeaf(index: number) { + return this.leafArray[index] + } + + get merkleRoot() { + return this.hashNodes[this.hashNodes.length - 1][0] + } + + getMerklePath(leafIndex: number) { + const leafNode = this.leafArray[leafIndex] + let prevHash = this.hashNodes[0] + const neighbor: string[] = [] + const neighborType: boolean[] = [] + + const leafNodeHash = hash160( + NftOpenMinterProto.nftMerkleLeafToString(leafNode) + ) + if (leafIndex < prevHash.length) { + prevHash[leafIndex] = leafNodeHash + } else { + prevHash.push(leafNodeHash) + } + + let prevIndex = leafIndex + + for (let i = 1; i < this.height; i++) { + prevHash = this.hashNodes[i - 1] + const curHash = this.hashNodes[i] + + const curIndex = Math.floor(prevIndex / 2) + // right node + if (prevIndex % 2 === 1) { + neighbor.push(prevHash[prevIndex - 1]) + neighborType.push(RIGHT_FLAG) + } else { + // left node + if (curIndex >= curHash.length) { + neighbor.push(this.emptyHashs[i - 1]) + neighborType.push(LEFT_FLAG) + } else { + if (prevHash.length > prevIndex + 1) { + neighbor.push(prevHash[prevIndex + 1]) + neighborType.push(LEFT_FLAG) + } else { + neighbor.push(this.emptyHashs[i - 1]) + neighborType.push(LEFT_FLAG) + } + } + } + prevIndex = curIndex + } + neighbor.push('') + neighborType.push(false) + return { + leaf: leafNodeHash, + leafNode: leafNode, + neighbor, + neighborType, + merkleRoot: this.merkleRoot, + } + } + + updateLeaf(leaf: NftMerkleLeaf, leafIndex: number) { + const oldLeaf = this.leafArray[leafIndex] + this.leafArray[leafIndex] = leaf + // return merkle path + const { neighbor, neighborType } = this.updateMerkleTree( + leaf, + leafIndex + ) + return { + oldLeaf, + neighbor, + neighborType, + leafIndex, + newLeaf: leaf, + merkleRoot: this.merkleRoot, + } + } + + updateMerkleTree(leaf: NftMerkleLeaf, leafIndex: number) { + let prevHash = this.hashNodes[0] + const neighbor: string[] = [] + const neighborType: boolean[] = [] + if (leafIndex < prevHash.length) { + prevHash[leafIndex] = hash160( + NftOpenMinterProto.nftMerkleLeafToString(leaf) + ) + } else { + prevHash.push( + hash160(NftOpenMinterProto.nftMerkleLeafToString(leaf)) + ) + } + + let prevIndex = leafIndex + + for (let i = 1; i < this.height; i++) { + prevHash = this.hashNodes[i - 1] + const curHash = this.hashNodes[i] + + const curIndex = Math.floor(prevIndex / 2) + // right node + if (prevIndex % 2 === 1) { + const newHash = hash160( + prevHash[prevIndex - 1] + prevHash[prevIndex] + ) + curHash[curIndex] = newHash + neighbor.push(prevHash[prevIndex - 1]) + neighborType.push(RIGHT_FLAG) + } else { + // left node + // new add + let newHash + if (curIndex >= curHash.length) { + newHash = hash160( + prevHash[prevIndex] + this.emptyHashs[i - 1] + ) + if (curHash.length !== curIndex) { + throw Error('wrong curHash') + } + curHash.push(newHash) + neighbor.push(this.emptyHashs[i - 1]) + neighborType.push(LEFT_FLAG) + } else { + if (prevHash.length > prevIndex + 1) { + newHash = hash160( + prevHash[prevIndex] + prevHash[prevIndex + 1] + ) + neighbor.push(prevHash[prevIndex + 1]) + neighborType.push(LEFT_FLAG) + } else { + newHash = hash160( + prevHash[prevIndex] + this.emptyHashs[i - 1] + ) + neighbor.push(this.emptyHashs[i - 1]) + neighborType.push(LEFT_FLAG) + } + curHash[curIndex] = newHash + } + } + prevIndex = curIndex + } + neighbor.push('') + neighborType.push(false) + return { neighbor, neighborType } + } + + private buildMerkleTree() { + this.hashNodes = [] + let prevHash: string[] = [] + let curHash: string[] = [] + + for (let i = 0; i < this.leafArray.length; i++) { + prevHash.push( + hash160( + NftOpenMinterProto.nftMerkleLeafToString(this.leafArray[i]) + ) + ) + } + if (prevHash.length > 0) { + this.hashNodes.push(prevHash) + } else { + this.hashNodes.push([this.emptyHashs[0]]) + } + + for (let i = 1; i < this.height; i++) { + prevHash = this.hashNodes[i - 1] + curHash = [] + for (let j = 0; j < prevHash.length; ) { + if (j + 1 < prevHash.length) { + curHash.push(hash160(prevHash[j] + prevHash[j + 1])) + } else { + curHash.push(hash160(prevHash[j] + this.emptyHashs[i - 1])) + } + j += 2 + } + this.hashNodes.push(curHash) + } + } +} diff --git a/packages/smartcontracts/src/contracts/nft/nftTransferGuard.ts b/packages/smartcontracts/src/contracts/nft/nftTransferGuard.ts new file mode 100644 index 00000000..030ca195 --- /dev/null +++ b/packages/smartcontracts/src/contracts/nft/nftTransferGuard.ts @@ -0,0 +1,130 @@ +import { + ByteString, + FixedArray, + SmartContract, + assert, + fill, + hash160, + int2ByteString, + len, + method, + sha256, + toByteString, +} from 'scrypt-ts' +import { + PrevoutsCtx, + SHPreimage, + SigHashUtils, + SpentScriptsCtx, +} from '../utils/sigHashUtils' +import { + MAX_INPUT, + MAX_STATE, + MAX_TOKEN_OUTPUT, + TxUtil, + int32, +} from '../utils/txUtil' +import { XrayedTxIdPreimg3 } from '../utils/txProof' +import { NftGuardConstState, NftGuardProto } from './nftGuardProto' +import { StateUtils, TxoStateHashes } from '../utils/stateUtils' + +export class NftTransferGuard extends SmartContract { + @method() + public transfer( + curTxoStateHashes: TxoStateHashes, + // nft owner address or other output locking script + ownerAddrOrScriptList: FixedArray, + localIdList: FixedArray, + nftOutputMaskList: FixedArray, + outputSatoshisList: FixedArray, + nftSatoshis: ByteString, + + // verify preTx data part + preState: NftGuardConstState, + // check deploy tx + preTx: XrayedTxIdPreimg3, + // + shPreimage: SHPreimage, + prevoutsCtx: PrevoutsCtx, + spentScripts: SpentScriptsCtx + ) { + // Check sighash 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 + ) + // check preTx + StateUtils.verifyGuardStateHash( + preTx, + prevoutsCtx.spentTxhash, + NftGuardProto.stateHash(preState) + ) + // sum input amount + const localIdArray: FixedArray = fill( + -1n, + MAX_TOKEN_OUTPUT + ) + let localIdArrayIndex = 0n + for (let i = 0; i < MAX_INPUT; i++) { + const script = spentScripts[i] + if (script == preState.collectionScript) { + localIdArray[Number(localIdArrayIndex)] = + preState.localIdArray[i] + localIdArrayIndex += 1n + } + } + let stateHashString = toByteString('') + let outputs = toByteString('') + let outputLocalIdArrayIndex = 0n + const nftOutput = TxUtil.buildOutput( + preState.collectionScript, + nftSatoshis + ) + // sum output amount, build nft outputs, build nft state hash + for (let i = 0; i < MAX_STATE; i++) { + const addrOrScript = ownerAddrOrScriptList[i] + if (nftOutputMaskList[i]) { + // nft owner address + const localId = localIdArray[Number(outputLocalIdArrayIndex)] + outputs = outputs + nftOutput + const nftStateHash = hash160( + hash160(addrOrScript + int2ByteString(localId)) + ) + assert(hash160(curTxoStateHashes[i]) == nftStateHash) + assert(localId >= 0n) + assert(localIdList[i] == localId) + stateHashString += nftStateHash + outputLocalIdArrayIndex += 1n + } else { + // other output locking script + assert(addrOrScript != preState.collectionScript) + stateHashString += hash160(curTxoStateHashes[i]) + if (len(addrOrScript) > 0) { + outputs += TxUtil.buildOutput( + addrOrScript, + outputSatoshisList[i] + ) + } + } + } + assert(localIdArrayIndex == outputLocalIdArrayIndex) + const stateOutput = TxUtil.buildOpReturnRoot( + TxUtil.getStateScript(hash160(stateHashString)) + ) + const hashOutputs = sha256(stateOutput + outputs) + assert(hashOutputs == shPreimage.hashOutputs, 'hashOutputs mismatch') + } +} diff --git a/packages/smartcontracts/src/index.ts b/packages/smartcontracts/src/index.ts index 396511f6..c17e1e81 100644 --- a/packages/smartcontracts/src/index.ts +++ b/packages/smartcontracts/src/index.ts @@ -1,4 +1,3 @@ -import { join } from 'path' import { BurnGuard } from './contracts/token/burnGuard' import { ClosedMinter } from './contracts/token/closedMinter' import { OpenMinter } from './contracts/token/openMinter' @@ -13,13 +12,33 @@ import cat20 from '../artifacts/contracts/token/cat20.json' import burnGuard from '../artifacts/contracts/token/burnGuard.json' import transferGuard from '../artifacts/contracts/token/transferGuard.json' -(() => { +import { NftClosedMinter } from './contracts/nft/nftClosedMinter' +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 nftOpenMinter from '../artifacts/contracts/nft/nftOpenMinter.json' +import cat721 from '../artifacts/contracts/nft/cat721.json' +import nftTransferGuard from '../artifacts/contracts/nft/nftTransferGuard.json' +import nftBurnGuard from '../artifacts/contracts/nft/nftBurnGuard.json' +;(() => { + // token minter ClosedMinter.loadArtifact(closedMinter) OpenMinter.loadArtifact(openMinter) OpenMinterV2.loadArtifact(openMinterV2) + // token CAT20.loadArtifact(cat20) BurnGuard.loadArtifact(burnGuard) TransferGuard.loadArtifact(transferGuard) + // nft minter + NftClosedMinter.loadArtifact(nftClosedMinter) + NftOpenMinter.loadArtifact(nftOpenMinter) + // nft + CAT721.loadArtifact(cat721) + NftBurnGuard.loadArtifact(nftBurnGuard) + NftTransferGuard.loadArtifact(nftTransferGuard) })() export * from './contracts/token/closedMinter' export * from './contracts/token/cat20' @@ -32,6 +51,16 @@ export * from './contracts/token/openMinter' export * from './contracts/token/openMinterV2' export * from './contracts/token/openMinterProto' export * from './contracts/token/openMinterV2Proto' +export * from './contracts/nft/nftClosedMinter' +export * from './contracts/nft/nftOpenMinter' +export * from './contracts/nft/nftClosedMinterProto' +export * from './contracts/nft/nftOpenMinterProto' +export * from './contracts/nft/nftOpenMinterMerkleTree' +export * from './contracts/nft/cat721' +export * from './contracts/nft/cat721Proto' +export * from './contracts/nft/nftBurnGuard' +export * from './contracts/nft/nftTransferGuard' +export * from './contracts/nft/nftGuardProto' export * from './contracts/utils/txUtil' export * from './contracts/utils/txProof' export * from './contracts/utils/stateUtils' @@ -42,3 +71,5 @@ export * from './lib/proof' export * from './lib/txTools' export * from './lib/commit' export * from './lib/guardInfo' +export * from './lib/btc' +export * from './lib/catTx' diff --git a/packages/smartcontracts/src/lib/commit.ts b/packages/smartcontracts/src/lib/commit.ts index bfb907a9..12ac0cf1 100644 --- a/packages/smartcontracts/src/lib/commit.ts +++ b/packages/smartcontracts/src/lib/commit.ts @@ -59,11 +59,11 @@ export function chunks(bin: T[], chunkSize: number): T[][] { export const getCatCommitScript = ( leafKeyXPub: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any - contractMeta: Record + metadata: Record ) => { const m = new Map() - for (const key in contractMeta) { - m.set(key, contractMeta[key]) + for (const key in metadata) { + m.set(key, metadata[key]) } const data = Buffer.from(cbor2.encode(m)) @@ -91,3 +91,121 @@ export const getCatCommitScript = ( return Buffer.concat(res).toString('hex') } + + + +export const getCatCollectionCommitScript = ( + leafKeyXPub: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata: Record, + content?: { + type: string, + body: string, + } +) => { + + const res = [] + + res.push( + toPushData(Buffer.from(leafKeyXPub, 'hex')) // 0 OP_IF "cat" + ) + + res.push(btc.Script.fromASM('OP_CHECKSIGVERIFY').toBuffer()) // checkSig + res.push(btc.Script.fromASM('OP_2DROP OP_2DROP OP_DROP').toBuffer()) // drop all stateHashes in the witness + res.push(btc.Script.fromASM('OP_0 OP_IF 636174').toBuffer()) // cat protocal envelope start + res.push(btc.Script.fromASM('OP_2').toBuffer()) // cat NFT collection + + if (Object.keys(metadata).length > 0) { + const m = new Map() + for (const key in metadata) { + m.set(key, metadata[key]) + } + + const metadataChunks = chunks(Array.from(Buffer.from(cbor2.encode(m))), limit) + + // if the metadata exceeds the limit of 520, it is split into multiple chunks. + for (const chunk of metadataChunks) { + res.push(toPushData(Buffer.from([5]))) + res.push(toPushData(Buffer.from(chunk))) + } + } + + if(content) { + + res.push(toPushData(Buffer.from([1]))) + + res.push(toPushData(Buffer.from(content.type, 'utf-8'))) + + res.push(Buffer.from([0])) + + const dataChunks = chunks(Array.from(Buffer.from(content.body, 'hex')), limit) + + // if the contentBody exceeds the limit of 520, it is split into multiple chunks. + for (const chunk of dataChunks) { + res.push(toPushData(Buffer.from(chunk))) + } + + } + + res.push(btc.Script.fromASM('OP_ENDIF').toBuffer()) // cat protocal envelope end + + res.push(btc.Script.fromASM('OP_1').toBuffer()) // put true on top stack + + return Buffer.concat(res).toString('hex') +} + + +export const getCatNFTCommitScript = ( + leafKeyXPub: string, + metadata: Record, + content?: { + type: string, + body: string, + }, +) => { + const res = [] + res.push( + toPushData(Buffer.from(leafKeyXPub, 'hex')) // 0 OP_IF "cat" + ) + + res.push(btc.Script.fromASM('OP_CHECKSIGVERIFY').toBuffer()) // checkSig + res.push(btc.Script.fromASM('OP_0 OP_IF 636174').toBuffer()) // cat protocal envelope start + res.push(btc.Script.fromASM('OP_3').toBuffer()) // cat NFT + + if (Object.keys(metadata).length > 0) { + const m = new Map() + for (const key in metadata) { + m.set(key, metadata[key]) + } + + const metadataChunks = chunks(Array.from(Buffer.from(cbor2.encode(m))), limit) + + // if the metadata exceeds the limit of 520, it is split into multiple chunks. + for (const chunk of metadataChunks) { + res.push(toPushData(Buffer.from([5]))) + res.push(toPushData(Buffer.from(chunk))) + } + } + + if(content) { + + res.push(toPushData(Buffer.from([1]))) + + res.push(toPushData(Buffer.from(content.type, 'utf-8'))) + + res.push(Buffer.from([0])) + + const dataChunks = chunks(Array.from(Buffer.from(content.body, 'hex')), limit) + + // if the contentBody exceeds the limit of 520, it is split into multiple chunks. + for (const chunk of dataChunks) { + res.push(toPushData(Buffer.from(chunk))) + } + } + + res.push(btc.Script.fromASM('OP_ENDIF').toBuffer()) // cat protocal envelope end + + res.push(btc.Script.fromASM('OP_1').toBuffer()) // put true on top stack + + return Buffer.concat(res).toString('hex') +} \ No newline at end of file diff --git a/packages/smartcontracts/src/lib/guardInfo.ts b/packages/smartcontracts/src/lib/guardInfo.ts index 9362f7c4..b8e43e70 100644 --- a/packages/smartcontracts/src/lib/guardInfo.ts +++ b/packages/smartcontracts/src/lib/guardInfo.ts @@ -1,5 +1,7 @@ import { BurnGuard } from '../index' import { TransferGuard } from '../index' +import { NftBurnGuard } from '../index' +import { NftTransferGuard } from '../index' import { TaprootMastSmartContract } from './catTx' export const getGuardContractInfo = function (): TaprootMastSmartContract { @@ -12,3 +14,14 @@ export const getGuardContractInfo = function (): TaprootMastSmartContract { const guardInfo = new TaprootMastSmartContract(contractMap) return guardInfo } + +export const getNftGuardContractInfo = function (): TaprootMastSmartContract { + const nftBurnGuard = new NftBurnGuard() + const nftTransfer = new NftTransferGuard() + const contractMap = { + burn: nftBurnGuard, + transfer: nftTransfer, + } + const guardInfo = new TaprootMastSmartContract(contractMap) + return guardInfo +} diff --git a/packages/smartcontracts/tests/nft/cat721.test.ts b/packages/smartcontracts/tests/nft/cat721.test.ts new file mode 100644 index 00000000..57d065b7 --- /dev/null +++ b/packages/smartcontracts/tests/nft/cat721.test.ts @@ -0,0 +1,494 @@ +import { expect } from 'chai' +import { NftClosedMinter } from '../../src/contracts/nft/nftClosedMinter' +import { + NftClosedMinterProto, + NftClosedMinterState, +} from '../../src/contracts/nft/nftClosedMinterProto' +import { CAT721, NftGuardInfo } from '../../src/contracts/nft/cat721' +import { CAT721Proto, CAT721State } from '../../src/contracts/nft/cat721Proto' +import { NftGuardProto } from '../../src/contracts/nft/nftGuardProto' +import { NftBurnGuard } from '../../src/contracts/nft/nftBurnGuard' +import { NftTransferGuard } from '../../src/contracts/nft/nftTransferGuard' +import { btc } from '../../src/lib/btc' +import { + CatTx, + ContractIns, + TaprootMastSmartContract, + TaprootSmartContract, +} from '../../src/lib/catTx' +import { KeyInfo, getKeyInfoFromWif, getPrivKey } from '../utils/privateKey' +import { + UTXO, + getBtcDummyUtxo, + getDummyGenesisTx, + getDummySigner, + getDummyUTXO, +} from '../utils/txHelper' + +import { + getOutpointObj, + getOutpointString, + getTxCtx, +} from '../../src/lib/txTools' +import { nftClosedMinterCall, nftClosedMinterDeploy } from './closedMinter' +import { MethodCallOptions, fill, toByteString } from 'scrypt-ts' +import { + emptyTokenArray, + getBackTraceInfoSearch, + getTxHeaderCheck, +} from '../../src/lib/proof' +import { unlockTaprootContractInput } from '../utils/contractUtils' +import { getNftGuardContractInfo, nftGuardDeloy } from './cat721' +import { + MAX_INPUT, + MAX_TOKEN_INPUT, + MAX_TOKEN_OUTPUT, +} from '../../src/contracts/utils/txUtil' + +export async function nftTransferCall( + feeGuardUtxo, + feeTokenUtxo, + seckey, + pubKeyPrefix, + pubkeyX, + collectionNfts: ContractIns[], + receivers: CAT721State[], + minterScript: string, + guardInfo: TaprootMastSmartContract, + burn: boolean, + options: { + errorNftSeq?: boolean + errorGuardLocalId?: boolean + errorMask?: boolean + } = {} +): Promise | null> { + const nftGuardState = NftGuardProto.createEmptyState() + nftGuardState.collectionScript = + collectionNfts[0].contractTaproot.lockingScriptHex + for (let index = 0; index < MAX_TOKEN_INPUT; index++) { + if (collectionNfts[index]) { + if (!options.errorGuardLocalId) { + nftGuardState.localIdArray[index] = + collectionNfts[index].state.localId + } + } + } + const nftGuardDeployInfo = await nftGuardDeloy( + feeGuardUtxo, + seckey, + nftGuardState, + guardInfo, + burn + ) + const catTx = CatTx.create() + for (const nft of collectionNfts) { + catTx.fromCatTx(nft.catTx, nft.atOutputIndex) + } + catTx.fromCatTx(nftGuardDeployInfo.catTx, nftGuardDeployInfo.atOutputIndex) + if (catTx.tx.inputs.length < MAX_INPUT) { + catTx.tx.from(feeTokenUtxo) + } + if (!burn) { + if (options.errorNftSeq) { + const temp1 = receivers[0] + const temp2 = receivers[1] + receivers[0] = temp2 + receivers[1] = temp1 + } + for (const receiver of receivers) { + catTx.addStateContractOutput( + nftGuardState.collectionScript, + CAT721Proto.toByteString(receiver) + ) + } + } + for (let i = 0; i < collectionNfts.length; i++) { + const nft = collectionNfts[i] + const { shPreimage, prevoutsCtx, spentScripts, sighash } = + await getTxCtx(catTx.tx, i, nft.contractTaproot.tapleafBuffer) + const sig = btc.crypto.Schnorr.sign(seckey, sighash.hash) + expect( + btc.crypto.Schnorr.verify(seckey.publicKey, sighash.hash, sig) + ).to.be.equal(true) + const preTx = nft.catTx.tx + const prePreTx = nft.preCatTx?.tx + const backtraceInfo = getBackTraceInfoSearch( + preTx, + prePreTx, + nft.contractTaproot.lockingScriptHex, + minterScript + ) + const amountCheckTx = getTxHeaderCheck(nftGuardDeployInfo.catTx.tx, 1) + const guardInputIndex = collectionNfts.length + const amountCheckInfo: NftGuardInfo = { + outputIndex: getOutpointObj(nftGuardDeployInfo.catTx.tx, 1) + .outputIndex, + inputIndexVal: BigInt(guardInputIndex), + tx: amountCheckTx.tx, + guardState: nftGuardDeployInfo.state, + } + await nft.contract.connect(getDummySigner()) + const nftCall = await nft.contract.methods.unlock( + { + isUserSpend: true, + userPubKeyPrefix: pubKeyPrefix, + userPubKey: pubkeyX, + userSig: sig.toString('hex'), + contractInputIndex: BigInt(collectionNfts.length + 1), + }, + nft.state, + nft.catTx.getPreState(), + amountCheckInfo, + backtraceInfo, + shPreimage, + prevoutsCtx, + spentScripts, + { + fromUTXO: getDummyUTXO(), + verify: false, + exec: false, + } as MethodCallOptions + ) + unlockTaprootContractInput( + nftCall, + nft.contractTaproot, + catTx.tx, + preTx, + i, + true, + true + ) + } + const { shPreimage, prevoutsCtx, spentScripts } = await getTxCtx( + catTx.tx, + collectionNfts.length, + nftGuardDeployInfo.contractTaproot.tapleafBuffer + ) + const preTx = getTxHeaderCheck(nftGuardDeployInfo.catTx.tx, 1) + await nftGuardDeployInfo.contract.connect(getDummySigner()) + if (!burn) { + const tokenOutputMaskArray = fill(false, MAX_TOKEN_OUTPUT) + const ownerAddrOrScriptArray = emptyTokenArray() + const localIdList = fill(0n, MAX_TOKEN_OUTPUT) + const outputSatoshiArray = emptyTokenArray() + for (let i = 0; i < receivers.length; i++) { + const receiver = receivers[i] + tokenOutputMaskArray[i] = true + ownerAddrOrScriptArray[i] = receiver.ownerAddr + localIdList[i] = receiver.localId + } + if (options.errorMask) { + tokenOutputMaskArray[receivers.length] = true + ownerAddrOrScriptArray[receivers.length] = receivers[0].ownerAddr + localIdList[receivers.length] = receivers[0].localId + } + const nftTransferCheckCall = + await nftGuardDeployInfo.contract.methods.transfer( + catTx.state.stateHashList, + ownerAddrOrScriptArray, + localIdList, + tokenOutputMaskArray, + outputSatoshiArray, + toByteString('4a01000000000000'), + nftGuardDeployInfo.state, + preTx.tx, + shPreimage, + prevoutsCtx, + spentScripts, + { + fromUTXO: getDummyUTXO(), + verify: false, + exec: false, + } as MethodCallOptions + ) + unlockTaprootContractInput( + nftTransferCheckCall, + nftGuardDeployInfo.contractTaproot, + catTx.tx, + nftGuardDeployInfo.catTx.tx, + collectionNfts.length, + true, + true + ) + } else { + { + const outputArray = emptyTokenArray() + const outputSatoshiArray = emptyTokenArray() + const burnGuardCall = + await nftGuardDeployInfo.contract.methods.burn( + catTx.state.stateHashList, + outputArray, + outputSatoshiArray, + nftGuardDeployInfo.state, + preTx.tx, + shPreimage, + prevoutsCtx, + { + fromUTXO: getDummyUTXO(), + verify: false, + exec: false, + } as MethodCallOptions + ) + unlockTaprootContractInput( + burnGuardCall, + nftGuardDeployInfo.contractTaproot, + catTx.tx, + nftGuardDeployInfo.catTx.tx, + collectionNfts.length, + true, + true + ) + } + } + return null +} + +describe('Test `CAT721` tokens', () => { + let keyInfo: KeyInfo + let genesisTx: btc.Transaction + let genesisUtxo: UTXO + let genesisOutpoint: string + let nftClosedMinter: NftClosedMinter + let nftClosedMinterTaproot: TaprootSmartContract + let nftGuardInfo: TaprootMastSmartContract + let nft: CAT721 + let initNftClosedMinterState: NftClosedMinterState + let nftClosedMinterState: NftClosedMinterState + let nftTaproot: TaprootSmartContract + let closedMinterIns: ContractIns + let feeGuardUtxo + let feeTokenUtxo + const collectionMax = 100n + // let closedMinterInsFake: ContractIns + + before(async () => { + // init load + await NftClosedMinter.loadArtifact() + await CAT721.loadArtifact() + await NftTransferGuard.loadArtifact() + await NftBurnGuard.loadArtifact() + // key info + keyInfo = getKeyInfoFromWif(getPrivKey()) + // dummy genesis + const dummyGenesis = getDummyGenesisTx(keyInfo.seckey, keyInfo.addr) + genesisTx = dummyGenesis.genesisTx + genesisUtxo = dummyGenesis.genesisUtxo + genesisOutpoint = getOutpointString(genesisTx, 0) + // minter + nftClosedMinter = new NftClosedMinter( + keyInfo.xAddress, + genesisOutpoint, + collectionMax + ) + nftClosedMinterTaproot = TaprootSmartContract.create(nftClosedMinter) + // guard + nftGuardInfo = getNftGuardContractInfo() + // nft + nft = new CAT721( + nftClosedMinterTaproot.lockingScriptHex, + nftGuardInfo.lockingScriptHex + ) + nftTaproot = TaprootSmartContract.create(nft) + initNftClosedMinterState = NftClosedMinterProto.create( + nftTaproot.lockingScriptHex, + collectionMax, + 0n + ) + nftClosedMinterState = initNftClosedMinterState + // deploy minter + closedMinterIns = await nftClosedMinterDeploy( + keyInfo.seckey, + genesisUtxo, + nftClosedMinter, + nftClosedMinterTaproot, + initNftClosedMinterState + ) + // const dummyFakeGenesis = getDummyGenesisTx(keyInfo.seckey, keyInfo.addr) + // minter + // const fakeClosedMinter = new NftClosedMinter( + // keyInfo.xAddress, + // getOutpointString(dummyFakeGenesis.genesisTx, 0) + // ) + // const fakeClosedMinterTaproot = + // TaprootSmartContract.create(fakeClosedMinter) + // closedMinterInsFake = await nftClosedMinterDeploy( + // keyInfo.seckey, + // dummyFakeGenesis.genesisUtxo, + // fakeClosedMinter, + // fakeClosedMinterTaproot, + // initNftClosedMinterState + // ) + // closedMinterInsUpgrade + // const dummyGenesisUpgrade = getDummyGenesisTx( + // keyInfo.seckey, + // keyInfo.addr + // ) + // const closedMinterUpgrade = new NftClosedMinter( + // keyInfo.xAddress, + // getOutpointString(dummyGenesisUpgrade.genesisTx, 0) + // ) + // const closedMinterUpgradeTaproot = + // TaprootSmartContract.create(closedMinterUpgrade) + // upgrade token + // const upgradeToken = new CAT721( + // closedMinterUpgradeTaproot.lockingScriptHex + // ) + // const upgradeTokenTaproot = TaprootSmartContract.create(upgradeToken) + // closedMinterInsUpgrade = await nftClosedMinterDeploy( + // keyInfo.seckey, + // dummyGenesisUpgrade.genesisUtxo, + // closedMinterUpgrade, + // upgradeTokenTaproot, + // initNftClosedMinterState + // ) + feeGuardUtxo = getBtcDummyUtxo(keyInfo.addr) + feeTokenUtxo = getBtcDummyUtxo(keyInfo.addr) + // wrongFeeTokenUtxo = getBtcDummyUtxo( + // new btc.PrivateKey().toAddress( + // null, + // btc.Address.PayToWitnessPublicKeyHash + // ) + // ) + }) + + async function mintNft(nftState: CAT721State) { + const closedMinterCallInfo = await nftClosedMinterCall( + closedMinterIns, + nftTaproot, + nftState + ) + closedMinterIns = closedMinterCallInfo + .nexts[0] as ContractIns + nftClosedMinterState.nextLocalId += 1n + return closedMinterCallInfo.nexts[1] as ContractIns + } + + async function getNftByNumber( + count: number + ): Promise[]> { + const collectionNfts: ContractIns[] = [] + for (let i = 0; i < count; i++) { + collectionNfts.push( + await mintNft( + CAT721Proto.create( + keyInfo.xAddress, + nftClosedMinterState.nextLocalId + ) + ) + ) + } + return collectionNfts + } + + describe('When a nft is being transferred by users', () => { + it('t01: should succeed with any input index and output index', async () => { + for (let index = 0; index < MAX_TOKEN_INPUT; index++) { + const nfts = await getNftByNumber(index + 1) + const nftStateList = nfts.map((value) => value.state) + await nftTransferCall( + feeGuardUtxo, + feeTokenUtxo, + keyInfo.seckey, + keyInfo.pubKeyPrefix, + keyInfo.pubkeyX, + nfts, + nftStateList, + nftClosedMinterTaproot.lockingScriptHex, + nftGuardInfo, + false + ) + } + }) + + it('t02: should fail when inputs localIds different outputs localIds', async () => { + for (let index = 0; index < MAX_TOKEN_INPUT; index++) { + const nfts = await getNftByNumber(index + 1) + const nftStateList = nfts.map((value) => value.state) + await expect( + nftTransferCall( + feeGuardUtxo, + feeTokenUtxo, + keyInfo.seckey, + keyInfo.pubKeyPrefix, + keyInfo.pubkeyX, + nfts, + nftStateList, + nftClosedMinterTaproot.lockingScriptHex, + nftGuardInfo, + false, + { + errorNftSeq: true, + } + ) + ).to.be.rejected + } + }) + + it('t03: should burt success', async () => { + for (let index = 0; index < MAX_TOKEN_INPUT; index++) { + const nfts = await getNftByNumber(index + 1) + const nftStateList = nfts.map((value) => value.state) + await nftTransferCall( + feeGuardUtxo, + feeTokenUtxo, + keyInfo.seckey, + keyInfo.pubKeyPrefix, + keyInfo.pubkeyX, + nfts, + nftStateList, + nftClosedMinterTaproot.lockingScriptHex, + nftGuardInfo, + true + ) + } + }) + + it('t04: should failed guard with error localId', async () => { + for (let index = 0; index < MAX_TOKEN_INPUT; index++) { + const nfts = await getNftByNumber(index + 1) + const nftStateList = nfts.map((value) => value.state) + await expect( + nftTransferCall( + feeGuardUtxo, + feeTokenUtxo, + keyInfo.seckey, + keyInfo.pubKeyPrefix, + keyInfo.pubkeyX, + nfts, + nftStateList, + nftClosedMinterTaproot.lockingScriptHex, + nftGuardInfo, + false, + { + errorGuardLocalId: true, + } + ) + ).to.be.rejected + } + }) + + it('t04: should failed guard with error mask', async () => { + for (let index = 0; index < MAX_TOKEN_INPUT; index++) { + const nfts = await getNftByNumber(index + 1) + const nftStateList = nfts.map((value) => value.state) + await expect( + nftTransferCall( + feeGuardUtxo, + feeTokenUtxo, + keyInfo.seckey, + keyInfo.pubKeyPrefix, + keyInfo.pubkeyX, + nfts, + nftStateList, + nftClosedMinterTaproot.lockingScriptHex, + nftGuardInfo, + false, + { + errorGuardLocalId: true, + } + ) + ).to.be.rejected + } + }) + }) +}) diff --git a/packages/smartcontracts/tests/nft/cat721.ts b/packages/smartcontracts/tests/nft/cat721.ts new file mode 100644 index 00000000..462205cf --- /dev/null +++ b/packages/smartcontracts/tests/nft/cat721.ts @@ -0,0 +1,46 @@ +import { NftBurnGuard } from '../../src/contracts/nft/nftBurnGuard' +import { + NftGuardConstState, + NftGuardProto, +} from '../../src/contracts/nft/nftGuardProto' +import { NftTransferGuard } from '../../src/contracts/nft/nftTransferGuard' +import { CatTx, TaprootMastSmartContract } from '../../src/lib/catTx' + +export const getNftGuardContractInfo = function () { + const burnGuard = new NftBurnGuard() + const transfer = new NftTransferGuard() + const contractMap = { + burn: burnGuard, + transfer: transfer, + } + const guardInfo = new TaprootMastSmartContract(contractMap) + return guardInfo +} + +export async function nftGuardDeloy( + feeUtxo, + seckey, + guardState: NftGuardConstState, + guardInfo: TaprootMastSmartContract, + burn: boolean +) { + const catTx = CatTx.create() + catTx.tx.from(feeUtxo) + const locking = guardInfo.lockingScript + const atIndex = catTx.addStateContractOutput( + locking, + NftGuardProto.toByteString(guardState) + ) + catTx.sign(seckey) + return { + catTx: catTx, + contract: burn + ? guardInfo.contractTaprootMap.burn.contract + : guardInfo.contractTaprootMap.transfer.contract, + state: guardState, + contractTaproot: burn + ? guardInfo.contractTaprootMap.burn + : guardInfo.contractTaprootMap.transfer, + atOutputIndex: atIndex, + } +} diff --git a/packages/smartcontracts/tests/nft/closedMinter.test.ts b/packages/smartcontracts/tests/nft/closedMinter.test.ts new file mode 100644 index 00000000..72a6c52d --- /dev/null +++ b/packages/smartcontracts/tests/nft/closedMinter.test.ts @@ -0,0 +1,273 @@ +import * as dotenv from 'dotenv' +dotenv.config() +import { expect, use } from 'chai' +import { NftClosedMinter } from '../../src/contracts/nft/nftClosedMinter' +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 { nftClosedMinterCall, nftClosedMinterDeploy } from './closedMinter' +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 { + NftClosedMinterProto, + NftClosedMinterState, +} from '../../src/contracts/nft/nftClosedMinterProto' +import { CAT721Proto } 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 `NftClosedMinter`', () => { + let nftClosedMinter: NftClosedMinter + let nftClosedMinterTaproot: TaprootSmartContract + let initNftClosedMinterState: NftClosedMinterState + let nftClosedMinterState: NftClosedMinterState + let contractIns: ContractIns + const collectionMax = 100n + before(async () => { + await NftClosedMinter.loadArtifact() + nftClosedMinter = new NftClosedMinter( + xAddress, + genesisOutpoint, + collectionMax + ) + nftClosedMinterTaproot = TaprootSmartContract.create(nftClosedMinter) + initNftClosedMinterState = NftClosedMinterProto.create( + nftScript, + collectionMax, + 0n + ) + nftClosedMinterState = initNftClosedMinterState + contractIns = await nftClosedMinterDeploy( + 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 nftClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState + ) + await closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + genesisTx + ) + expect(callInfo.nexts.length).to.be.equal(2) + }) + + it('should admin mint nft until end.', async () => { + // tx call + let prePreTx = genesisTx + while (nftClosedMinterState.nextLocalId <= collectionMax) { + // nft state + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + nftClosedMinterState.nextLocalId + ) + const callInfo = await nftClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState + ) + await closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx + ) + prePreTx = contractIns.catTx.tx + if (callInfo.nexts.length > 1) { + contractIns = callInfo + .nexts[0] as ContractIns + expect(callInfo.nexts).to.be.length(2) + } else { + break + } + nftClosedMinterState.nextLocalId += 1n + } + }) + + it('should failed mint nft with error nextLockId', async () => { + let prePreTx = genesisTx + while (nftClosedMinterState.nextLocalId <= collectionMax) { + // nft state + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + nftClosedMinterState.nextLocalId + ) + const callInfo = await nftClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState + ) + await expect( + closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx + ) + ).to.be.rejected + prePreTx = contractIns.catTx.tx + if (callInfo.nexts.length > 1) { + contractIns = callInfo + .nexts[0] as ContractIns + expect(callInfo.nexts).to.be.length(2) + } else { + break + } + nftClosedMinterState.nextLocalId += 2n + } + }) + + 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 nftClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState + ) + 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 + expect(callInfo.nexts).to.be.length(2) + } else { + break + } + nftClosedMinterState.nextLocalId += 1n + } + }) +}) diff --git a/packages/smartcontracts/tests/nft/closedMinter.ts b/packages/smartcontracts/tests/nft/closedMinter.ts new file mode 100644 index 00000000..0a88ca99 --- /dev/null +++ b/packages/smartcontracts/tests/nft/closedMinter.ts @@ -0,0 +1,122 @@ +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 { + NftClosedMinterProto, + NftClosedMinterState, +} from '../../src/contracts/nft/nftClosedMinterProto' +import { FixedArray } from 'scrypt-ts' + +export async function nftClosedMinterDeploy( + seckey, + genesisUtxo, + nftClosedMinter: NftClosedMinter, + nftClosedMinterTaproot: TaprootSmartContract, + nftClosedMinterState: NftClosedMinterState +): Promise> { + // tx deploy + const catTx = CatTx.create() + catTx.tx.from([genesisUtxo]) + const atIndex = catTx.addStateContractOutput( + nftClosedMinterTaproot.lockingScript, + NftClosedMinterProto.toByteString(nftClosedMinterState) + ) + catTx.sign(seckey) + return { + catTx: catTx, + contract: nftClosedMinter, + state: nftClosedMinterState, + contractTaproot: nftClosedMinterTaproot, + atOutputIndex: atIndex, + } +} + +export async function nftClosedMinterDeployQuota( + seckey, + genesisUtxo, + nftClosedMinter: NftClosedMinter, + nftClosedMinterTaproot: TaprootSmartContract, + nftClosedMinterStateList: FixedArray +): Promise, 5>> { + // tx deploy + const catTx = CatTx.create() + catTx.tx.from([genesisUtxo]) + const nexts = [] as unknown as FixedArray< + ContractIns, + 5 + > + for (let index = 0; index < nftClosedMinterStateList.length; index++) { + const nftClosedMinterState = nftClosedMinterStateList[index] + const atIndex = catTx.addStateContractOutput( + nftClosedMinterTaproot.lockingScript, + NftClosedMinterProto.toByteString(nftClosedMinterState) + ) + nexts.push({ + catTx: catTx, + contract: nftClosedMinter, + state: nftClosedMinterState, + contractTaproot: nftClosedMinterTaproot, + atOutputIndex: atIndex, + }) + } + catTx.sign(seckey) + return nexts +} + +export async function nftClosedMinterCall( + contractIns: ContractIns, + nftTaproot: TaprootSmartContract, + nftState: CAT721State +): Promise> { + const catTx = CatTx.create() + const atInputIndex = catTx.fromCatTx( + contractIns.catTx, + contractIns.atOutputIndex + ) + const nexts: ContractIns[] = [] + // + const nextLocalId = contractIns.state.nextLocalId + 1n + if (nextLocalId < contractIns.state.quotaMaxLocalId) { + const nextState = NftClosedMinterProto.create( + contractIns.state.nftScript, + contractIns.state.quotaMaxLocalId, + contractIns.state.nextLocalId + 1n + ) + const atOutputIndex = catTx.addStateContractOutput( + contractIns.contractTaproot.lockingScript, + NftClosedMinterProto.toByteString(nextState) + ) + nexts.push({ + catTx: catTx, + contract: contractIns.contract, + state: contractIns.state, + 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/closedMinterQuota.test.ts b/packages/smartcontracts/tests/nft/closedMinterQuota.test.ts new file mode 100644 index 00000000..e9b291b8 --- /dev/null +++ b/packages/smartcontracts/tests/nft/closedMinterQuota.test.ts @@ -0,0 +1,186 @@ +import * as dotenv from 'dotenv' +dotenv.config() +import { expect, use } from 'chai' +import { NftClosedMinter } from '../../src/contracts/nft/nftClosedMinter' +import chaiAsPromised from 'chai-as-promised' +import { FixedArray, 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 { nftClosedMinterCall, nftClosedMinterDeployQuota } from './closedMinter' +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 { + NftClosedMinterProto, + NftClosedMinterState, +} from '../../src/contracts/nft/nftClosedMinterProto' +import { CAT721Proto } 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 `NftClosedMinter` quota', () => { + let nftClosedMinter: NftClosedMinter + let nftClosedMinterTaproot: TaprootSmartContract + let initNftClosedMinterStateList: FixedArray + let nftClosedMinterStateList: FixedArray + let contractInsList: FixedArray, 5> + const collectionMax = 100n + before(async () => { + await NftClosedMinter.loadArtifact() + nftClosedMinter = new NftClosedMinter( + xAddress, + genesisOutpoint, + collectionMax + ) + nftClosedMinterTaproot = TaprootSmartContract.create(nftClosedMinter) + const quotaNumber = 5n + const quotaStep = collectionMax / quotaNumber + initNftClosedMinterStateList = [] as unknown as FixedArray< + NftClosedMinterState, + 5 + > + for (let index = 0; index < collectionMax; index += Number(quotaStep)) { + initNftClosedMinterStateList[index / Number(quotaStep)] = + NftClosedMinterProto.create( + nftScript, + BigInt(index) + quotaStep, + BigInt(index) + ) + } + nftClosedMinterStateList = initNftClosedMinterStateList + contractInsList = await nftClosedMinterDeployQuota( + seckey, + genesisUtxo, + nftClosedMinter, + nftClosedMinterTaproot, + initNftClosedMinterStateList + ) + }) + + it('should admin parallel mint nft until end.', async () => { + // tx call + for (let index = 0; index < contractInsList.length; index++) { + const nftClosedMinterState = nftClosedMinterStateList[index] + let contractIns = contractInsList[index] + let prePreTx = genesisTx + while ( + nftClosedMinterState.nextLocalId <= + nftClosedMinterState.quotaMaxLocalId + ) { + // nft state + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + nftClosedMinterState.nextLocalId + ) + const callInfo = await nftClosedMinterCall( + contractIns, + nftClosedMinterTaproot, + nftState + ) + await closedMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx + ) + prePreTx = contractIns.catTx.tx + if (callInfo.nexts.length > 1) { + contractIns = callInfo + .nexts[0] as ContractIns + expect(callInfo.nexts).to.be.length(2) + } else { + break + } + nftClosedMinterState.nextLocalId += 1n + } + } + }) +}) diff --git a/packages/smartcontracts/tests/nft/nftOpenMinter.test.ts b/packages/smartcontracts/tests/nft/nftOpenMinter.test.ts new file mode 100644 index 00000000..f4eec8be --- /dev/null +++ b/packages/smartcontracts/tests/nft/nftOpenMinter.test.ts @@ -0,0 +1,336 @@ +import * as dotenv from 'dotenv' +dotenv.config() +import { expect, use } from 'chai' +import { NftClosedMinter } from '../../src/contracts/nft/nftClosedMinter' +import { NftOpenMinter } from '../../src/contracts/nft/nftOpenMinter' +import chaiAsPromised from 'chai-as-promised' +import { MethodCallOptions, hash160, toByteString } from 'scrypt-ts' +import { getOutpointString } from '../../src/lib/txTools' +import { + getBtcDummyUtxo, + getDummyGenesisTx, + getDummySigner, + getDummyUTXO, +} from '../utils/txHelper' +import { getKeyInfoFromWif, getPrivKey } from '../utils/privateKey' +import { + CatTx, + ContractCallResult, + ContractIns, + TaprootSmartContract, + script2P2TR, +} from '../../src/lib/catTx' +import { getBackTraceInfo } from '../../src/lib/proof' +import { unlockTaprootContractInput } from '../utils/contractUtils' +import { btc } from '../../src/lib/btc' +import { + NftMerkleLeaf, + NftOpenMinterMerkleTreeData, + NftOpenMinterProto, + NftOpenMinterState, +} from '../../src/contracts/nft/nftOpenMinterProto' +import { CAT721Proto } from '../../src/contracts/nft/cat721Proto' +import { getCatCommitScript } from '../../src/lib/commit' +import { HEIGHT } from '../../src/contracts/nft/nftOpenMinterMerkleTree' +import { nftOpenMinterCall, nftOpenMinterDeploy } from './openMinter' +use(chaiAsPromised) + +const DUST = toByteString('4a01000000000000') + +// 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' + +const createDummyCommitScript = function (localId: number): NftMerkleLeaf { + const commitScript = getCatCommitScript(keyInfo.pubkeyX, { localId }) + const lockingScript = Buffer.from(commitScript, 'hex') + const { p2tr: p2trCommit } = script2P2TR(lockingScript) + return { + commitScript: p2trCommit, + localId: BigInt(localId), + isMined: false, + } +} + +const generateCollectionLeaf = function (max: number) { + const nftMerkleLeafList: NftMerkleLeaf[] = [] + for (let index = 0; index < max; index++) { + nftMerkleLeafList.push(createDummyCommitScript(index)) + } + return nftMerkleLeafList +} + +export async function deployNftCommitContract(feeUtxo, seckey, lockingScript) { + const catTx = CatTx.create() + catTx.tx.from(feeUtxo) + const atIndex = catTx.addContractOutput(lockingScript) + catTx.sign(seckey) + return { + catTx: catTx, + contract: null, + state: null, + contractTaproot: null, + atOutputIndex: atIndex, + } +} + +export async function openMinterUnlock( + callInfo: ContractCallResult, + preCatTx: CatTx, + seckey, + nftState, + preNftClosedMinterState, + pubkeyX, + pubKeyPrefix, + prePreTx, + neighbor, + neighborType +) { + 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, + neighbor, + neighborType, + pubKeyPrefix, + pubkeyX, + () => 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 + ) +} + +describe('Test SmartContract `NftOpenMinter`', () => { + const collectionMax: number = 100 + let nftOpenMinter: NftOpenMinter + let nftOpenMinterTaproot: TaprootSmartContract + let initNftOpenMinterState: NftOpenMinterState + let contractIns: ContractIns + let nftOpenMinterMerkleTreeData: NftOpenMinterMerkleTreeData + let feeUtxo + + before(async () => { + await NftOpenMinter.loadArtifact() + nftOpenMinter = new NftOpenMinter( + genesisOutpoint, + BigInt(collectionMax), + 0n, + xAddress + ) + nftOpenMinterTaproot = TaprootSmartContract.create(nftOpenMinter) + nftOpenMinterMerkleTreeData = new NftOpenMinterMerkleTreeData( + generateCollectionLeaf(collectionMax), + HEIGHT + ) + initNftOpenMinterState = NftOpenMinterProto.create( + nftScript, + nftOpenMinterMerkleTreeData.merkleRoot, + 0n + ) + contractIns = await nftOpenMinterDeploy( + seckey, + genesisUtxo, + nftOpenMinter, + nftOpenMinterTaproot, + initNftOpenMinterState + ) + feeUtxo = getBtcDummyUtxo(keyInfo.addr) + }) + + it('should open mint nft collection pass.', async () => { + // tx call + // nft state + let prePreTx = genesisTx + for ( + let collectionIndex = 0; + collectionIndex < collectionMax; + collectionIndex++ + ) { + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + BigInt(collectionIndex) + ) + const callInfo = await nftOpenMinterCall( + seckey, + feeUtxo, + contractIns, + nftOpenMinterTaproot, + nftState, + collectionMax, + nftOpenMinterMerkleTreeData + ) + const leafInfo = nftOpenMinterMerkleTreeData.getMerklePath( + Number(collectionIndex) + ) + await openMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx, + leafInfo.neighbor, + leafInfo.neighborType + ) + prePreTx = contractIns.catTx.tx + if (callInfo.nexts.length > 1) { + contractIns = callInfo + .nexts[0] as ContractIns + expect(callInfo.nexts).to.be.length(2) + } else { + break + } + } + }) + + it('should failed mint nft with error nextLockId', async () => { + let prePreTx = genesisTx + for ( + let collectionIndex = 0; + collectionIndex < collectionMax; + collectionIndex += 2 + ) { + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + BigInt(collectionIndex) + ) + const callInfo = await nftOpenMinterCall( + seckey, + feeUtxo, + contractIns, + nftOpenMinterTaproot, + nftState, + collectionMax, + nftOpenMinterMerkleTreeData + ) + const leafInfo = nftOpenMinterMerkleTreeData.getMerklePath( + Number(collectionIndex) + ) + const call = openMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx, + leafInfo.neighbor, + leafInfo.neighborType + ) + if (collectionIndex > 0) { + await expect(call).to.be.rejected + } else { + try { + await call + } catch { + // + } + } + prePreTx = contractIns.catTx.tx + if (callInfo.nexts.length > 1) { + contractIns = callInfo + .nexts[0] as ContractIns + expect(callInfo.nexts).to.be.length(2) + } else { + break + } + } + }) + + it('should failed mint nft with error expected script', async () => { + let prePreTx = genesisTx + for ( + let collectionIndex = 0; + collectionIndex < collectionMax - 1; + collectionIndex++ + ) { + const nftState = CAT721Proto.create( + hash160(toByteString('00')), + BigInt(collectionIndex) + ) + const callInfo = await nftOpenMinterCall( + seckey, + feeUtxo, + contractIns, + nftOpenMinterTaproot, + nftState, + collectionMax, + nftOpenMinterMerkleTreeData, + { + errorLeafScript: true, + } + ) + const leafInfo = nftOpenMinterMerkleTreeData.getMerklePath( + Number(collectionIndex) + ) + await expect( + openMinterUnlock( + callInfo, + contractIns.catTx, + seckey, + nftState, + contractIns.state, + pubkeyX, + pubKeyPrefix, + prePreTx, + leafInfo.neighbor, + leafInfo.neighborType + ) + ).to.be.rejected + prePreTx = contractIns.catTx.tx + if (callInfo.nexts.length > 1) { + contractIns = callInfo + .nexts[0] as ContractIns + expect(callInfo.nexts).to.be.length(2) + } else { + break + } + } + }) +}) diff --git a/packages/smartcontracts/tests/nft/openMinter.ts b/packages/smartcontracts/tests/nft/openMinter.ts new file mode 100644 index 00000000..2a015d66 --- /dev/null +++ b/packages/smartcontracts/tests/nft/openMinter.ts @@ -0,0 +1,121 @@ +import { NftOpenMinter } from '../../src/contracts/nft/nftOpenMinter' +import { + CatTx, + ContractCallResult, + ContractIns, + TaprootSmartContract, +} from '../../src/lib/catTx' +import { CAT721Proto, CAT721State } from '../../src/contracts/nft/cat721Proto' +import { + NftMerkleLeaf, + NftOpenMinterMerkleTreeData, + NftOpenMinterProto, + NftOpenMinterState, +} from '../../src/contracts/nft/nftOpenMinterProto' +import { deployNftCommitContract } from './nftOpenMinter.test' + +export async function nftOpenMinterDeploy( + seckey, + genesisUtxo, + nftOpenMinter: NftOpenMinter, + nftOpenMinterTaproot: TaprootSmartContract, + nftOpenMinterState: NftOpenMinterState +): Promise> { + // tx deploy + const catTx = CatTx.create() + catTx.tx.from([genesisUtxo]) + const atIndex = catTx.addStateContractOutput( + nftOpenMinterTaproot.lockingScript, + NftOpenMinterProto.toByteString(nftOpenMinterState) + ) + catTx.sign(seckey) + return { + catTx: catTx, + contract: nftOpenMinter, + state: nftOpenMinterState, + contractTaproot: nftOpenMinterTaproot, + atOutputIndex: atIndex, + } +} + +export async function nftOpenMinterCall( + seckey, + feeUtxo, + contractIns: ContractIns, + nftTaproot: TaprootSmartContract, + nftState: CAT721State, + max: number, + nftOpenMinterMerkleTreeData: NftOpenMinterMerkleTreeData, + options: { + errorLeafScript?: boolean + } = {} +): Promise> { + const catTx = CatTx.create() + const atInputIndex = catTx.fromCatTx( + contractIns.catTx, + contractIns.atOutputIndex + ) + const nexts: ContractIns[] = [] + // + const collectionIndex = Number(nftState.localId) + const oldLeaf = nftOpenMinterMerkleTreeData.getLeaf( + Number(nftState.localId) + ) + let commitScript = oldLeaf.commitScript + if (options.errorLeafScript) { + commitScript = nftOpenMinterMerkleTreeData.getLeaf( + Number(nftState.localId) + 1 + ).commitScript + } + // add commit script + const commit = await deployNftCommitContract(feeUtxo, seckey, commitScript) + catTx.fromCatTx(commit.catTx, commit.atOutputIndex) + const newLeaf: NftMerkleLeaf = { + commitScript: oldLeaf.commitScript, + localId: oldLeaf.localId, + isMined: true, + } + const updateLeafInfo = nftOpenMinterMerkleTreeData.updateLeaf( + newLeaf, + collectionIndex + ) + const mintNumber = contractIns.state.nextLocalId + 1n + if (mintNumber != BigInt(max)) { + const nextState = NftOpenMinterProto.create( + contractIns.state.nftScript, + updateLeafInfo.merkleRoot, + mintNumber + ) + const atOutputIndex = catTx.addStateContractOutput( + contractIns.contractTaproot.lockingScript, + NftOpenMinterProto.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/cat20.test.ts b/packages/smartcontracts/tests/token/cat20.test.ts similarity index 98% rename from packages/smartcontracts/tests/cat20.test.ts rename to packages/smartcontracts/tests/token/cat20.test.ts index 4829e303..5f54e7d3 100644 --- a/packages/smartcontracts/tests/cat20.test.ts +++ b/packages/smartcontracts/tests/token/cat20.test.ts @@ -6,29 +6,33 @@ import { emptyTokenArray, getBackTraceInfoSearch, getTxHeaderCheck, -} from '../src/lib/proof' +} from '../../src/lib/proof' import chaiAsPromised from 'chai-as-promised' import { MethodCallOptions, fill, hash160, toByteString } from 'scrypt-ts' -import { getOutpointObj, getOutpointString, getTxCtx } from '../src/lib/txTools' -import { CAT20Proto, CAT20State } from '../src/contracts/token/cat20Proto' -import { GuardProto } from '../src/contracts/token/guardProto' -import { CAT20, GuardInfo } from '../src/contracts/token/cat20' -import { ClosedMinter } from '../src/contracts/token/closedMinter' -import { TransferGuard } from '../src/contracts/token/transferGuard' +import { + getOutpointObj, + getOutpointString, + getTxCtx, +} from '../../src/lib/txTools' +import { CAT20Proto, CAT20State } from '../../src/contracts/token/cat20Proto' +import { GuardProto } from '../../src/contracts/token/guardProto' +import { CAT20, GuardInfo } from '../../src/contracts/token/cat20' +import { ClosedMinter } from '../../src/contracts/token/closedMinter' +import { TransferGuard } from '../../src/contracts/token/transferGuard' import { UTXO, getBtcDummyUtxo, getDummyGenesisTx, getDummySigner, getDummyUTXO, -} from './utils/txHelper' +} from '../utils/txHelper' import { MAX_INPUT, MAX_TOKEN_INPUT, MAX_TOKEN_OUTPUT, -} from '../src/contracts/utils/txUtil' -import { KeyInfo, getKeyInfoFromWif, getPrivKey } from './utils/privateKey' -import { unlockTaprootContractInput } from './utils/contractUtils' +} from '../../src/contracts/utils/txUtil' +import { KeyInfo, getKeyInfoFromWif, getPrivKey } from '../utils/privateKey' +import { unlockTaprootContractInput } from '../utils/contractUtils' import { closedMinterCall, closedMinterDeploy, @@ -40,9 +44,9 @@ import { ContractIns, TaprootMastSmartContract, TaprootSmartContract, -} from '../src/lib/catTx' -import { BurnGuard } from '../src/contracts/token/burnGuard' -import { btc } from '../src/lib/btc' +} from '../../src/lib/catTx' +import { BurnGuard } from '../../src/contracts/token/burnGuard' +import { btc } from '../../src/lib/btc' use(chaiAsPromised) export async function tokenTransferCall( diff --git a/packages/smartcontracts/tests/cat20.ts b/packages/smartcontracts/tests/token/cat20.ts similarity index 91% rename from packages/smartcontracts/tests/cat20.ts rename to packages/smartcontracts/tests/token/cat20.ts index 00df7599..eb758f31 100644 --- a/packages/smartcontracts/tests/cat20.ts +++ b/packages/smartcontracts/tests/token/cat20.ts @@ -1,16 +1,19 @@ -import { ClosedMinter } from '../src/contracts/token/closedMinter' +import { ClosedMinter } from '../../src/contracts/token/closedMinter' import { CatTx, ContractCallResult, ContractIns, TaprootMastSmartContract, TaprootSmartContract, -} from '../src/lib/catTx' -import { CAT20Proto, CAT20State } from '../src/contracts/token/cat20Proto' +} from '../../src/lib/catTx' +import { CAT20Proto, CAT20State } from '../../src/contracts/token/cat20Proto' import { SmartContract } from 'scrypt-ts' -import { BurnGuard } from '../src/contracts/token/burnGuard' -import { TransferGuard } from '../src/contracts/token/transferGuard' -import { GuardProto, GuardConstState } from '../src/contracts/token/guardProto' +import { BurnGuard } from '../../src/contracts/token/burnGuard' +import { TransferGuard } from '../../src/contracts/token/transferGuard' +import { + GuardProto, + GuardConstState, +} from '../../src/contracts/token/guardProto' export type GetTokenScript = ( minterScript: string diff --git a/packages/smartcontracts/tests/closedMinter.test.ts b/packages/smartcontracts/tests/token/closedMinter.test.ts similarity index 91% rename from packages/smartcontracts/tests/closedMinter.test.ts rename to packages/smartcontracts/tests/token/closedMinter.test.ts index 85b6a218..6c0b18bb 100644 --- a/packages/smartcontracts/tests/closedMinter.test.ts +++ b/packages/smartcontracts/tests/token/closedMinter.test.ts @@ -1,26 +1,26 @@ import * as dotenv from 'dotenv' dotenv.config() import { expect, use } from 'chai' -import { ClosedMinter } from '../src/contracts/token/closedMinter' +import { ClosedMinter } from '../../src/contracts/token/closedMinter' import chaiAsPromised from 'chai-as-promised' import { MethodCallOptions, hash160, toByteString } from 'scrypt-ts' -import { getOutpointString } from '../src/lib/txTools' +import { getOutpointString } from '../../src/lib/txTools' import { getDummyGenesisTx, getDummySigner, getDummyUTXO, -} from './utils/txHelper' -import { CAT20Proto } from '../src/contracts/token/cat20Proto' -import { getKeyInfoFromWif, getPrivKey } from './utils/privateKey' +} from '../utils/txHelper' +import { CAT20Proto } from '../../src/contracts/token/cat20Proto' +import { getKeyInfoFromWif, getPrivKey } from '../utils/privateKey' import { GetTokenScript, closedMinterCall, closedMinterDeploy, } from './closedMinter' -import { CatTx, ContractCallResult, ContractIns } from '../src/lib/catTx' -import { getBackTraceInfo } from '../src/lib/proof' -import { unlockTaprootContractInput } from './utils/contractUtils' -import { btc } from '../src/lib/btc' +import { CatTx, ContractCallResult, ContractIns } from '../../src/lib/catTx' +import { getBackTraceInfo } from '../../src/lib/proof' +import { unlockTaprootContractInput } from '../utils/contractUtils' +import { btc } from '../../src/lib/btc' use(chaiAsPromised) const DUST = toByteString('4a01000000000000') diff --git a/packages/smartcontracts/tests/closedMinter.ts b/packages/smartcontracts/tests/token/closedMinter.ts similarity index 91% rename from packages/smartcontracts/tests/closedMinter.ts rename to packages/smartcontracts/tests/token/closedMinter.ts index 1a172e02..72d22f14 100644 --- a/packages/smartcontracts/tests/closedMinter.ts +++ b/packages/smartcontracts/tests/token/closedMinter.ts @@ -1,11 +1,11 @@ -import { ClosedMinter } from '../src/contracts/token/closedMinter' +import { ClosedMinter } from '../../src/contracts/token/closedMinter' import { CatTx, ContractCallResult, ContractIns, TaprootSmartContract, -} from '../src/lib/catTx' -import { CAT20Proto, CAT20State } from '../src/contracts/token/cat20Proto' +} from '../../src/lib/catTx' +import { CAT20Proto, CAT20State } from '../../src/contracts/token/cat20Proto' export type GetTokenScript = (minterScript: string) => Promise diff --git a/packages/smartcontracts/tests/closedMinterLegacy.test.ts b/packages/smartcontracts/tests/token/closedMinterLegacy.test.ts similarity index 89% rename from packages/smartcontracts/tests/closedMinterLegacy.test.ts rename to packages/smartcontracts/tests/token/closedMinterLegacy.test.ts index a9ed278a..e89818c2 100644 --- a/packages/smartcontracts/tests/closedMinterLegacy.test.ts +++ b/packages/smartcontracts/tests/token/closedMinterLegacy.test.ts @@ -1,26 +1,26 @@ import * as dotenv from 'dotenv' dotenv.config() import { expect, use } from 'chai' -import { ClosedMinter } from '../src/contracts/token/closedMinter' +import { ClosedMinter } from '../../src/contracts/token/closedMinter' import chaiAsPromised from 'chai-as-promised' import { MethodCallOptions, hash160, toByteString } from 'scrypt-ts' -import { getOutpointString } from '../src/lib/txTools' +import { getOutpointString } from '../../src/lib/txTools' import { getDummyGenesisTx, getDummySigner, getDummyUTXO, -} from './utils/txHelper' -import { CAT20Proto } from '../src/contracts/token/cat20Proto' -import { getLegacyKeyInfoFromWif, getPrivKey } from './utils/privateKey' +} from '../utils/txHelper' +import { CAT20Proto } from '../../src/contracts/token/cat20Proto' +import { getLegacyKeyInfoFromWif, getPrivKey } from '../utils/privateKey' import { GetTokenScript, closedMinterCall, closedMinterDeploy, } from './closedMinter' -import { CatTx, ContractCallResult, ContractIns } from '../src/lib/catTx' -import { getBackTraceInfo } from '../src/lib/proof' -import { unlockTaprootContractInput } from './utils/contractUtils' -import { btc } from '../src/lib/btc' +import { CatTx, ContractCallResult, ContractIns } from '../../src/lib/catTx' +import { getBackTraceInfo } from '../../src/lib/proof' +import { unlockTaprootContractInput } from '../utils/contractUtils' +import { btc } from '../../src/lib/btc' use(chaiAsPromised) const DUST = toByteString('4a01000000000000') diff --git a/packages/smartcontracts/tests/openMinter.test.ts b/packages/smartcontracts/tests/token/openMinter.test.ts similarity index 94% rename from packages/smartcontracts/tests/openMinter.test.ts rename to packages/smartcontracts/tests/token/openMinter.test.ts index 093b2b91..cc23fdbf 100644 --- a/packages/smartcontracts/tests/openMinter.test.ts +++ b/packages/smartcontracts/tests/token/openMinter.test.ts @@ -5,28 +5,28 @@ dotenv.config() import { expect, use } from 'chai' import chaiAsPromised from 'chai-as-promised' import { UTXO, hash160 } from 'scrypt-ts' -import { getOutpointString } from '../src/lib/txTools' -import { OpenMinter } from '../src/contracts/token/openMinter' -import { getBtcDummyUtxo, getDummyGenesisTx } from './utils/txHelper' +import { getOutpointString } from '../../src/lib/txTools' +import { OpenMinter } from '../../src/contracts/token/openMinter' +import { getBtcDummyUtxo, getDummyGenesisTx } from '../utils/txHelper' import { OpenMinterProto, OpenMinterState, -} from '../src/contracts/token/openMinterProto' -import { CAT20Proto, CAT20State } from '../src/contracts/token/cat20Proto' -import { KeyInfo, getKeyInfoFromWif, getPrivKey } from './utils/privateKey' +} from '../../src/contracts/token/openMinterProto' +import { CAT20Proto, CAT20State } from '../../src/contracts/token/cat20Proto' +import { KeyInfo, getKeyInfoFromWif, getPrivKey } from '../utils/privateKey' import { openMinterCall, openMinterDeploy } from './openMinter' import { CatTx, ContractIns, TaprootSmartContract, script2P2TR, -} from '../src/lib/catTx' -import { getCatCommitScript } from '../src/lib/commit' -import { CAT20 } from '../src/contracts/token/cat20' +} from '../../src/lib/catTx' +import { getCatCommitScript } from '../../src/lib/commit' +import { CAT20 } from '../../src/contracts/token/cat20' import { getGuardContractInfo } from './cat20' -import { TransferGuard } from '../src/contracts/token/transferGuard' -import { BurnGuard } from '../src/contracts/token/burnGuard' -import { btc } from '../src/lib/btc' +import { TransferGuard } from '../../src/contracts/token/transferGuard' +import { BurnGuard } from '../../src/contracts/token/burnGuard' +import { btc } from '../../src/lib/btc' use(chaiAsPromised) export interface TokenInfo { diff --git a/packages/smartcontracts/tests/openMinter.ts b/packages/smartcontracts/tests/token/openMinter.ts similarity index 90% rename from packages/smartcontracts/tests/openMinter.ts rename to packages/smartcontracts/tests/token/openMinter.ts index 63424b16..c0e9aa1e 100644 --- a/packages/smartcontracts/tests/openMinter.ts +++ b/packages/smartcontracts/tests/token/openMinter.ts @@ -1,23 +1,26 @@ -import { MAX_NEXT_MINTERS, OpenMinter } from '../src/contracts/token/openMinter' +import { + MAX_NEXT_MINTERS, + OpenMinter, +} from '../../src/contracts/token/openMinter' import { CatTx, ContractCallResult, ContractIns, TaprootSmartContract, -} from '../src/lib/catTx' +} from '../../src/lib/catTx' import { OpenMinterProto, OpenMinterState, -} from '../src/contracts/token/openMinterProto' -import { int32 } from '../src/contracts/utils/txUtil' -import { CAT20Proto, CAT20State } from '../src/contracts/token/cat20Proto' -import { getTxCtx } from '../src/lib/txTools' -import { getBackTraceInfo } from '../src/lib/proof' -import { getDummySigner, getDummyUTXO } from './utils/txHelper' -import { KeyInfo } from './utils/privateKey' +} from '../../src/contracts/token/openMinterProto' +import { int32 } from '../../src/contracts/utils/txUtil' +import { CAT20Proto, CAT20State } from '../../src/contracts/token/cat20Proto' +import { getTxCtx } from '../../src/lib/txTools' +import { getBackTraceInfo } from '../../src/lib/proof' +import { getDummySigner, getDummyUTXO } from '../utils/txHelper' +import { KeyInfo } from '../utils/privateKey' import { MethodCallOptions, toByteString } from 'scrypt-ts' -import { unlockTaprootContractInput } from './utils/contractUtils' -import { btc } from '../src/lib/btc' +import { unlockTaprootContractInput } from '../utils/contractUtils' +import { btc } from '../../src/lib/btc' export type GetTokenScript = (minterScript: string) => Promise diff --git a/packages/smartcontracts/tests/openMinterV2.test.ts b/packages/smartcontracts/tests/token/openMinterV2.test.ts similarity index 95% rename from packages/smartcontracts/tests/openMinterV2.test.ts rename to packages/smartcontracts/tests/token/openMinterV2.test.ts index 9e9e5c9e..d6bc7c30 100644 --- a/packages/smartcontracts/tests/openMinterV2.test.ts +++ b/packages/smartcontracts/tests/token/openMinterV2.test.ts @@ -5,28 +5,28 @@ dotenv.config() import { expect, use } from 'chai' import chaiAsPromised from 'chai-as-promised' import { UTXO, hash160 } from 'scrypt-ts' -import { getOutpointString } from '../src/lib/txTools' -import { OpenMinterV2 } from '../src/contracts/token/openMinterV2' -import { getBtcDummyUtxo, getDummyGenesisTx } from './utils/txHelper' +import { getOutpointString } from '../../src/lib/txTools' +import { OpenMinterV2 } from '../../src/contracts/token/openMinterV2' +import { getBtcDummyUtxo, getDummyGenesisTx } from '../utils/txHelper' import { OpenMinterV2Proto, OpenMinterV2State, -} from '../src/contracts/token/openMinterV2Proto' -import { CAT20Proto, CAT20State } from '../src/contracts/token/cat20Proto' -import { KeyInfo, getKeyInfoFromWif, getPrivKey } from './utils/privateKey' +} from '../../src/contracts/token/openMinterV2Proto' +import { CAT20Proto, CAT20State } from '../../src/contracts/token/cat20Proto' +import { KeyInfo, getKeyInfoFromWif, getPrivKey } from '../utils/privateKey' import { openMinterCall, openMinterV2Deploy } from './openMinterV2' import { CatTx, ContractIns, TaprootSmartContract, script2P2TR, -} from '../src/lib/catTx' -import { getCatCommitScript } from '../src/lib/commit' -import { CAT20 } from '../src/contracts/token/cat20' +} from '../../src/lib/catTx' +import { getCatCommitScript } from '../../src/lib/commit' +import { CAT20 } from '../../src/contracts/token/cat20' import { getGuardContractInfo } from './cat20' -import { TransferGuard } from '../src/contracts/token/transferGuard' -import { BurnGuard } from '../src/contracts/token/burnGuard' -import { btc } from '../src/lib/btc' +import { TransferGuard } from '../../src/contracts/token/transferGuard' +import { BurnGuard } from '../../src/contracts/token/burnGuard' +import { btc } from '../../src/lib/btc' use(chaiAsPromised) export interface TokenInfo { diff --git a/packages/smartcontracts/tests/openMinterV2.ts b/packages/smartcontracts/tests/token/openMinterV2.ts similarity index 90% rename from packages/smartcontracts/tests/openMinterV2.ts rename to packages/smartcontracts/tests/token/openMinterV2.ts index 3493b34c..b13a8a63 100644 --- a/packages/smartcontracts/tests/openMinterV2.ts +++ b/packages/smartcontracts/tests/token/openMinterV2.ts @@ -3,21 +3,21 @@ import { ContractCallResult, ContractIns, TaprootSmartContract, -} from '../src/lib/catTx' +} from '../../src/lib/catTx' import { OpenMinterV2Proto, OpenMinterV2State, -} from '../src/contracts/token/openMinterV2Proto' -import { int32 } from '../src/contracts/utils/txUtil' -import { CAT20Proto, CAT20State } from '../src/contracts/token/cat20Proto' -import { getTxCtx } from '../src/lib/txTools' -import { getBackTraceInfo } from '../src/lib/proof' -import { getDummySigner, getDummyUTXO } from './utils/txHelper' -import { KeyInfo } from './utils/privateKey' +} from '../../src/contracts/token/openMinterV2Proto' +import { int32 } from '../../src/contracts/utils/txUtil' +import { CAT20Proto, CAT20State } from '../../src/contracts/token/cat20Proto' +import { getTxCtx } from '../../src/lib/txTools' +import { getBackTraceInfo } from '../../src/lib/proof' +import { getDummySigner, getDummyUTXO } from '../utils/txHelper' +import { KeyInfo } from '../utils/privateKey' import { MethodCallOptions, toByteString } from 'scrypt-ts' -import { unlockTaprootContractInput } from './utils/contractUtils' -import { btc } from '../src/lib/btc' -import { OpenMinterV2 } from '../src/contracts/token/openMinterV2' +import { unlockTaprootContractInput } from '../utils/contractUtils' +import { btc } from '../../src/lib/btc' +import { OpenMinterV2 } from '../../src/contracts/token/openMinterV2' export type GetTokenScript = (minterScript: string) => Promise diff --git a/packages/tracker/package.json b/packages/tracker/package.json index f2674d4b..8ebbed14 100644 --- a/packages/tracker/package.json +++ b/packages/tracker/package.json @@ -1,6 +1,6 @@ { "name": "@cat-protocol/cat-tracker", - "version": "0.1.0", + "version": "0.2.0", "description": "", "author": "catprotocol.org", "private": true, @@ -34,7 +34,7 @@ }, "dependencies": { "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3", - "@cat-protocol/cat-smartcontracts": "0.1.2", + "@cat-protocol/cat-smartcontracts": "0.2.1", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", @@ -47,7 +47,7 @@ "cbor": "^9.0.2", "lru-cache": "^11.0.1", "pg": "^8.12.0", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.1", "rxjs": "^7.8.1", "typeorm": "^0.3.20" }, diff --git a/packages/tracker/src/app-api.module.ts b/packages/tracker/src/app-api.module.ts index 44a3a827..ad16cac8 100644 --- a/packages/tracker/src/app-api.module.ts +++ b/packages/tracker/src/app-api.module.ts @@ -9,6 +9,7 @@ import { HealthCheckModule } from './routes/healthCheck/healthCheck.module'; import { TokenModule } from './routes/token/token.module'; import { MinterModule } from './routes/minter/minter.module'; import { AddressModule } from './routes/address/address.module'; +import { CollectionModule } from './routes/collection/collection.module'; // serivces import { CommonModule } from './services/common/common.module'; @@ -26,6 +27,7 @@ require('dotenv').config(); TokenModule, MinterModule, AddressModule, + CollectionModule, CommonModule, ], diff --git a/packages/tracker/src/common/constants.ts b/packages/tracker/src/common/constants.ts index 5c0f6ea7..06f9b211 100644 --- a/packages/tracker/src/common/constants.ts +++ b/packages/tracker/src/common/constants.ts @@ -5,7 +5,7 @@ export class Constants { static readonly CACHE_MAX_SIZE = 10000; - static readonly TOKEN_INFO_CACHE_BLOCKS_THRESHOLD = 120; + static readonly CACHE_AFTER_N_BLOCKS = 120; static readonly TAPROOT_LOCKING_SCRIPT_LENGTH = 34; @@ -33,7 +33,8 @@ export class Constants { static readonly MINTER_INPUT_WITNESS_AMOUNT_OFFSET = 6; - static readonly TOKEN_INFO_ENVELOPE = /OP_0 OP_IF 636174 OP_1 (.*?) OP_ENDIF/; + static readonly TOKEN_INFO_ENVELOPE = + /OP_0 OP_IF 636174 (OP_1|OP_2|OP_3) (.+?) OP_ENDIF/; static readonly TOKEN_AMOUNT_MAX_BYTES = 4; diff --git a/packages/tracker/src/common/types.ts b/packages/tracker/src/common/types.ts index 9e089f73..2ad78590 100644 --- a/packages/tracker/src/common/types.ts +++ b/packages/tracker/src/common/types.ts @@ -1,13 +1,3 @@ -export interface TokenInfo { - name: string; - symbol: string; - decimals: number; - max?: bigint; - limit?: bigint; - premine?: bigint; - minterMd5?: string; -} - export interface BlockHeader { hash: string; version: number; @@ -25,3 +15,37 @@ export interface BlockHeader { nTx: number; nextblockhash: string; } + +export enum TokenTypeScope { + Fungible, + NonFungible, + All, +} + +export enum EnvelopeMarker { + Token = 'OP_1', + Collection = 'OP_2', + NFT = 'OP_3', +} + +export interface Content { + type?: string; + encoding?: string; + raw?: Buffer; +} + +export interface EnvelopeData { + metadata?: object; + content?: Content; +} + +export interface TokenInfoEnvelope { + marker: EnvelopeMarker; + data: EnvelopeData; +} + +export interface TaprootPayment { + pubkey?: Buffer; + redeemScript?: Buffer; + witness?: Buffer[]; +} diff --git a/packages/tracker/src/common/utils.ts b/packages/tracker/src/common/utils.ts index 0cc0a928..ccf5530e 100644 --- a/packages/tracker/src/common/utils.ts +++ b/packages/tracker/src/common/utils.ts @@ -2,7 +2,7 @@ import { payments, script } from 'bitcoinjs-lib'; import { Constants, network } from './constants'; import { hash160 } from 'bitcoinjs-lib/src/crypto'; import { decode as cborDecode } from 'cbor'; -import { TokenInfo } from './types'; +import { EnvelopeMarker, EnvelopeData, TokenInfoEnvelope } from './types'; export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -60,29 +60,95 @@ export function ownerAddressToPubKeyHash(ownerAddr: string) { } } -export function parseTokenInfo(redeemScript: Buffer): TokenInfo | null { +export function parseTokenInfoEnvelope( + redeemScript: Buffer, +): TokenInfoEnvelope | null { try { const asm = script.toASM(redeemScript || Buffer.alloc(0)); const match = asm.match(Constants.TOKEN_INFO_ENVELOPE); - if (match && match[1]) { - const cborBuffer = Buffer.from(match[1].replaceAll(' ', ''), 'hex'); - const tokenInfo = cborDecode(cborBuffer); - if ( - tokenInfo['name'] !== undefined && - tokenInfo['symbol'] !== undefined && - tokenInfo['decimals'] !== undefined - ) { - return tokenInfo as TokenInfo; + if (match && match[1] && match[2]) { + switch (match[1]) { + case EnvelopeMarker.Token: + const cborBuffer = Buffer.from(match[2].replaceAll(' ', ''), 'hex'); + const metadata = cborDecode(cborBuffer); + if ( + metadata && + metadata['name'] !== undefined && + metadata['symbol'] !== undefined && + metadata['decimals'] !== undefined + ) { + return { + marker: EnvelopeMarker.Token, + data: { metadata }, + }; + } + break; + case EnvelopeMarker.Collection: + const info = parseEnvelope(match[2]); + if ( + info && + info.metadata && + info.metadata['name'] !== undefined && + info.metadata['symbol'] !== undefined + ) { + return { + marker: EnvelopeMarker.Collection, + data: info, + }; + } + break; + case EnvelopeMarker.NFT: + return { + marker: EnvelopeMarker.NFT, + data: parseEnvelope(match[2]), + }; } } } catch (e) { - throw new Error(`parse token info error, ${e.message}`); + throw new Error(`parse token info envelope error, ${e.message}`); } return null; } -export interface TaprootPayment { - pubkey?: Buffer; - redeemScript?: Buffer; - witness?: Buffer[]; +function parseEnvelope(envelope: string): EnvelopeData | null { + const items = envelope.split(' '); + let i = 0; + let contentRaw: Buffer | undefined = undefined; + let contentType: string | undefined = undefined; + let contentEncoding: string | undefined = undefined; + let metadataHex: string = ''; + while (i < items.length - 1) { + if (items[i] === '00' || items[i] === 'OP_0') { + // content raw + contentRaw = Buffer.from(items.slice(i + 1).join(''), 'hex'); + break; + } else if (items[i] === '01' || items[i] === 'OP_1') { + // content type + contentType = Buffer.from(items[i + 1], 'hex').toString('utf8'); + i += 2; + } else if (items[i] === '05' || items[i] === 'OP_5') { + // metadata + metadataHex += items[i + 1]; + i += 2; + } else if (items[i] === '09' || items[i] === 'OP_9') { + // content encoding + contentEncoding = Buffer.from(items[i + 1], 'hex').toString('utf8'); + i += 2; + } else { + i++; + } + } + const metadata = + metadataHex === '' + ? undefined + : cborDecode(Buffer.from(metadataHex, 'hex')); + let content = undefined; + if (contentRaw || contentType || contentEncoding) { + content = { + raw: contentRaw, + type: contentType, + encoding: contentEncoding, + }; + } + return metadata || content ? { metadata, content } : null; } diff --git a/packages/tracker/src/entities/nftInfo.entity.ts b/packages/tracker/src/entities/nftInfo.entity.ts new file mode 100644 index 00000000..f3d64f70 --- /dev/null +++ b/packages/tracker/src/entities/nftInfo.entity.ts @@ -0,0 +1,42 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + PrimaryColumn, +} from 'typeorm'; + +@Entity('nft_info') +export class NftInfoEntity { + @PrimaryColumn({ name: 'collection_id' }) + collectionId: string; + + @PrimaryColumn({ name: 'local_id', type: 'bigint' }) + localId: bigint; + + @Column({ name: 'mint_txid', length: 64 }) + @Index() + mintTxid: string; + + @Column({ name: 'mint_height' }) + @Index() + mintHeight: number; + + @Column({ name: 'commit_txid', length: 64 }) + commitTxid: string; + + @Column({ type: 'jsonb', nullable: true }) + metadata: object; + + @Column({ name: 'content_type', nullable: true }) + contentType: string; + + @Column({ name: 'content_encoding', nullable: true }) + contentEncoding: string; + + @Column({ name: 'content_raw', type: 'bytea', nullable: true }) + contentRaw: Buffer; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/packages/tracker/src/entities/tokenInfo.entity.ts b/packages/tracker/src/entities/tokenInfo.entity.ts index 996d8c49..f3b5c33f 100644 --- a/packages/tracker/src/entities/tokenInfo.entity.ts +++ b/packages/tracker/src/entities/tokenInfo.entity.ts @@ -1,4 +1,3 @@ -import { TokenInfo } from '../common/types'; import { Column, CreateDateColumn, @@ -34,7 +33,16 @@ export class TokenInfoEntity { decimals: number; @Column({ name: 'raw_info', type: 'jsonb' }) - rawInfo: TokenInfo; + rawInfo: object; + + @Column({ name: 'content_type', nullable: true }) + contentType: string; + + @Column({ name: 'content_encoding', nullable: true }) + contentEncoding: string; + + @Column({ name: 'content_raw', type: 'bytea', nullable: true }) + contentRaw: Buffer; @Column({ name: 'minter_pubkey', length: 64 }) @Index() diff --git a/packages/tracker/src/migrations/1729246840899-alter.ts b/packages/tracker/src/migrations/1729246840899-alter.ts new file mode 100644 index 00000000..0417e15b --- /dev/null +++ b/packages/tracker/src/migrations/1729246840899-alter.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Alter1729246840899 implements MigrationInterface { + name = 'Alter1729246840899'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE IF NOT EXISTS "nft_info" ("collection_id" character varying NOT NULL, "local_id" bigint NOT NULL, "mint_txid" character varying(64) NOT NULL, "mint_height" integer NOT NULL, "commit_txid" character varying(64) NOT NULL, "metadata" jsonb, "content_type" character varying, "content_encoding" character varying, "content_raw" bytea, "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_e1eaf4029498024bfc744325bb8" PRIMARY KEY ("collection_id", "local_id"))`, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_43f2d6d3ac593d8a883d14d3a9" ON "nft_info" ("mint_txid") `, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_d7adb9bb7a79091f94373b43e9" ON "nft_info" ("mint_height") `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "public"."IDX_d7adb9bb7a79091f94373b43e9"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_43f2d6d3ac593d8a883d14d3a9"`, + ); + await queryRunner.query(`DROP TABLE "nft_info"`); + } +} diff --git a/packages/tracker/src/migrations/1729444309666-alter.ts b/packages/tracker/src/migrations/1729444309666-alter.ts new file mode 100644 index 00000000..3d1a28ad --- /dev/null +++ b/packages/tracker/src/migrations/1729444309666-alter.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Alter1729444309666 implements MigrationInterface { + name = 'Alter1729444309666'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "token_info" ADD COLUMN IF NOT EXISTS "content_type" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "token_info" ADD COLUMN IF NOT EXISTS "content_encoding" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "token_info" ADD COLUMN IF NOT EXISTS "content_raw" bytea`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "token_info" DROP COLUMN "content_raw"`, + ); + await queryRunner.query( + `ALTER TABLE "token_info" DROP COLUMN "content_encoding"`, + ); + await queryRunner.query( + `ALTER TABLE "token_info" DROP COLUMN "content_type"`, + ); + } +} diff --git a/packages/tracker/src/routes/address/address.controller.ts b/packages/tracker/src/routes/address/address.controller.ts index 283e094b..84e830d0 100644 --- a/packages/tracker/src/routes/address/address.controller.ts +++ b/packages/tracker/src/routes/address/address.controller.ts @@ -7,21 +7,49 @@ import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; export class AddressController { constructor(private readonly addressService: AddressService) {} - @Get(':ownerAddr/balances') + @Get(':ownerAddrOrPkh/balances') @ApiTags('address') @ApiOperation({ summary: 'Get token balances by owner address' }) @ApiParam({ - name: 'ownerAddr', + name: 'ownerAddrOrPkh', required: true, type: String, - description: 'token owner address', + description: 'token owner address or public key hash', }) - async getTokenBalances(@Param('ownerAddr') ownerAddr: string) { + async getTokenBalances(@Param('ownerAddrOrPkh') ownerAddrOrPkh: string) { try { - const balances = await this.addressService.getTokenBalances(ownerAddr); + const balances = + await this.addressService.getTokenBalances(ownerAddrOrPkh); return okResponse(balances); } catch (e) { return errorResponse(e); } } + + @Get(':ownerAddrOrPkh/collections') + @ApiTags('address') + @ApiOperation({ summary: 'Get collection balances by owner address' }) + @ApiParam({ + name: 'ownerAddrOrPkh', + required: true, + type: String, + description: 'collection owner address or public key hash', + }) + async getCollectionBalances(@Param('ownerAddrOrPkh') ownerAddrOrPkh: string) { + try { + const balances = + await this.addressService.getCollectionBalances(ownerAddrOrPkh); + return okResponse({ + collections: balances.balances.map((balance) => { + return { + collectionId: balance.tokenId, + confirmed: balance.confirmed, + }; + }), + trackerBlockHeight: balances.trackerBlockHeight, + }); + } catch (e) { + return errorResponse(e); + } + } } diff --git a/packages/tracker/src/routes/address/address.service.ts b/packages/tracker/src/routes/address/address.service.ts index 06a4cfc9..55e7c1e4 100644 --- a/packages/tracker/src/routes/address/address.service.ts +++ b/packages/tracker/src/routes/address/address.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { TokenService } from '../token/token.service'; import { xOnlyPubKeyToAddress } from '../../common/utils'; import { CommonService } from '../../services/common/common.service'; +import { TokenTypeScope } from '../../common/types'; @Injectable() export class AddressService { @@ -10,19 +11,30 @@ export class AddressService { private readonly tokenService: TokenService, ) {} - async getTokenBalances(ownerAddr: string) { + async getTokenBalances(ownerAddrOrPkh: string) { + return this.getBalances(ownerAddrOrPkh, TokenTypeScope.Fungible); + } + + async getCollectionBalances(ownerAddrOrPkh: string) { + return this.getBalances(ownerAddrOrPkh, TokenTypeScope.NonFungible); + } + + private async getBalances(ownerAddrOrPkh: string, scope: TokenTypeScope) { const lastProcessedHeight = await this.commonService.getLastProcessedBlockHeight(); const utxos = await this.tokenService.queryTokenUtxosByOwnerAddress( lastProcessedHeight, - ownerAddr, + ownerAddrOrPkh, ); - const tokenBalances = this.tokenService.groupTokenBalances(utxos); + const tokenBalances = await this.tokenService.groupTokenBalances(utxos); const balances = []; for (const tokenPubKey in tokenBalances) { const tokenAddr = xOnlyPubKeyToAddress(tokenPubKey); const tokenInfo = - await this.tokenService.getTokenInfoByTokenIdOrTokenAddress(tokenAddr); + await this.tokenService.getTokenInfoByTokenIdOrTokenAddress( + tokenAddr, + scope, + ); if (tokenInfo) { balances.push({ tokenId: tokenInfo.tokenId, diff --git a/packages/tracker/src/routes/collection/collection.controller.ts b/packages/tracker/src/routes/collection/collection.controller.ts new file mode 100644 index 00000000..67229dcc --- /dev/null +++ b/packages/tracker/src/routes/collection/collection.controller.ts @@ -0,0 +1,261 @@ +import { Controller, Get, Param, Query, Res } from '@nestjs/common'; +import { CollectionService } from './collection.service'; +import { okResponse, errorResponse } from '../../common/utils'; +import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { TokenService } from '../token/token.service'; +import { Response } from 'express'; +import { TokenTypeScope } from '../../common/types'; + +@Controller('collections') +export class CollectionController { + constructor( + private readonly collectionService: CollectionService, + private readonly tokenService: TokenService, + ) {} + + @Get(':collectionIdOrAddr') + @ApiTags('collection') + @ApiOperation({ + summary: 'Get collection info by collection id or collection address', + }) + @ApiParam({ + name: 'collectionIdOrAddr', + required: true, + type: String, + description: 'collection id or collection address', + }) + async getCollectionInfo( + @Param('collectionIdOrAddr') collectionIdOrAddr: string, + ) { + try { + const collectionInfo = + await this.tokenService.getTokenInfoByTokenIdOrTokenAddress( + collectionIdOrAddr, + TokenTypeScope.NonFungible, + ); + if (collectionInfo) { + Object.assign(collectionInfo, { + collectionId: collectionInfo.tokenId, + collectionAddr: collectionInfo.tokenAddr, + collectionPubKey: collectionInfo.tokenPubKey, + metadata: collectionInfo.info, + }); + delete collectionInfo.tokenId; + delete collectionInfo.tokenAddr; + delete collectionInfo.tokenPubKey; + delete collectionInfo.info; + delete collectionInfo.decimals; + } + return okResponse(collectionInfo); + } catch (e) { + return errorResponse(e); + } + } + + @Get(':collectionIdOrAddr/content') + @ApiTags('collection') + @ApiOperation({ summary: 'Get collection content' }) + @ApiParam({ + name: 'collectionIdOrAddr', + required: true, + type: String, + description: 'collection id or collection address', + }) + async getCollectionContent( + @Param('collectionIdOrAddr') collectionIdOrAddr: string, + @Res() res: Response, + ) { + try { + const content = + await this.collectionService.getCollectionContent(collectionIdOrAddr); + if (content.type) { + res.setHeader('Content-Type', content.type); + } + if (content.encoding) { + res.setHeader('Content-Encoding', content.encoding); + } + res.send(content.raw); + } catch (e) { + return errorResponse(e); + } + } + + @Get(':collectionIdOrAddr/localId/:localId') + @ApiTags('collection') + @ApiOperation({ summary: 'Get nft info' }) + @ApiParam({ + name: 'collectionIdOrAddr', + required: true, + type: String, + description: 'collection id or collection address', + }) + @ApiParam({ + name: 'localId', + required: true, + type: Number, + description: 'nft local id', + }) + async getNftInfo( + @Param('collectionIdOrAddr') collectionIdOrAddr: string, + @Param('localId') localId: bigint, + ) { + try { + const nftInfo = await this.collectionService.getNftInfo( + collectionIdOrAddr, + localId, + ); + return okResponse(nftInfo); + } catch (e) { + return errorResponse(e); + } + } + + @Get(':collectionIdOrAddr/localId/:localId/content') + @ApiTags('collection') + @ApiOperation({ summary: 'Get nft content' }) + @ApiParam({ + name: 'collectionIdOrAddr', + required: true, + type: String, + description: 'collection id or collection address', + }) + @ApiParam({ + name: 'localId', + required: true, + type: Number, + description: 'nft local id', + }) + async getNftContent( + @Param('collectionIdOrAddr') collectionIdOrAddr: string, + @Param('localId') localId: bigint, + @Res() res: Response, + ) { + try { + const content = await this.collectionService.getNftContent( + collectionIdOrAddr, + localId, + ); + if (content.type) { + res.setHeader('Content-Type', content.type); + } + if (content.encoding) { + res.setHeader('Content-Encoding', content.encoding); + } + res.send(content.raw); + } catch (e) { + return errorResponse(e); + } + } + + @Get(':collectionIdOrAddr/localId/:localId/utxo') + @ApiTags('collection') + @ApiOperation({ summary: 'Get nft utxo' }) + @ApiParam({ + name: 'collectionIdOrAddr', + required: true, + type: String, + description: 'collection id or collection address', + }) + @ApiParam({ + name: 'localId', + required: true, + type: Number, + description: 'nft local id', + }) + async getNftUtxo( + @Param('collectionIdOrAddr') collectionIdOrAddr: string, + @Param('localId') localId: bigint, + ) { + try { + const utxo = await this.collectionService.getNftUtxo( + collectionIdOrAddr, + localId, + ); + return okResponse(utxo); + } catch (e) { + return errorResponse(e); + } + } + + @Get(':collectionIdOrAddr/addresses/:ownerAddrOrPkh/utxos') + @ApiTags('collection') + @ApiOperation({ summary: 'Get collection utxos by owner address' }) + @ApiParam({ + name: 'collectionIdOrAddr', + required: true, + type: String, + description: 'collection id or collection address', + }) + @ApiParam({ + name: 'ownerAddrOrPkh', + required: true, + type: String, + description: 'collection owner address or public key hash', + }) + @ApiQuery({ + name: 'offset', + required: false, + type: Number, + description: 'paging offset', + }) + @ApiQuery({ + name: 'limit', + required: false, + type: Number, + description: 'paging limit', + }) + async getCollectionUtxosByOwnerAddress( + @Param('collectionIdOrAddr') collectionIdOrAddr: string, + @Param('ownerAddrOrPkh') ownerAddrOrPkh: string, + @Query('offset') offset?: number, + @Query('limit') limit?: number, + ) { + try { + const utxos = await this.tokenService.getTokenUtxosByOwnerAddress( + collectionIdOrAddr, + TokenTypeScope.NonFungible, + ownerAddrOrPkh, + offset, + limit, + ); + return okResponse(utxos); + } catch (e) { + return errorResponse(e); + } + } + + @Get(':collectionIdOrAddr/addresses/:ownerAddrOrPkh/utxoCount') + @ApiTags('collection') + @ApiOperation({ summary: 'Get collection utxo count by owner address' }) + @ApiParam({ + name: 'collectionIdOrAddr', + required: true, + type: String, + description: 'collection id or collection address', + }) + @ApiParam({ + name: 'ownerAddrOrPkh', + required: true, + type: String, + description: 'collection owner address or public key hash', + }) + async getCollectionBalanceByOwnerAddress( + @Param('collectionIdOrAddr') collectionIdOrAddr: string, + @Param('ownerAddrOrPkh') ownerAddrOrPkh: string, + ) { + try { + const balance = await this.tokenService.getTokenBalanceByOwnerAddress( + collectionIdOrAddr, + TokenTypeScope.NonFungible, + ownerAddrOrPkh, + ); + return okResponse({ + collectionId: balance.tokenId, + confirmed: balance.confirmed, + trackerBlockHeight: balance.trackerBlockHeight, + }); + } catch (e) { + return errorResponse(e); + } + } +} diff --git a/packages/tracker/src/routes/collection/collection.module.ts b/packages/tracker/src/routes/collection/collection.module.ts new file mode 100644 index 00000000..8eb77e99 --- /dev/null +++ b/packages/tracker/src/routes/collection/collection.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { CollectionService } from './collection.service'; +import { CollectionController } from './collection.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CommonModule } from '../../services/common/common.module'; +import { TokenModule } from '../token/token.module'; +import { TxOutEntity } from '../../entities/txOut.entity'; +import { NftInfoEntity } from '../../entities/nftInfo.entity'; +import { TokenInfoEntity } from '../../entities/tokenInfo.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([TxOutEntity, NftInfoEntity, TokenInfoEntity]), + CommonModule, + TokenModule, + ], + providers: [CollectionService], + controllers: [CollectionController], + exports: [CollectionService], +}) +export class CollectionModule {} diff --git a/packages/tracker/src/routes/collection/collection.service.ts b/packages/tracker/src/routes/collection/collection.service.ts new file mode 100644 index 00000000..3f4954c2 --- /dev/null +++ b/packages/tracker/src/routes/collection/collection.service.ts @@ -0,0 +1,188 @@ +import { Injectable } from '@nestjs/common'; +import { CommonService } from '../../services/common/common.service'; +import { TokenService } from '../token/token.service'; +import { IsNull, LessThanOrEqual, Repository } from 'typeorm'; +import { TxOutEntity } from '../../entities/txOut.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { LRUCache } from 'lru-cache'; +import { NftInfoEntity } from '../../entities/nftInfo.entity'; +import { Constants } from '../../common/constants'; +import { Content, TokenTypeScope } from '../../common/types'; +import { TokenInfoEntity } from '../../entities/tokenInfo.entity'; + +@Injectable() +export class CollectionService { + private static readonly nftInfoCache = new LRUCache({ + max: Constants.CACHE_MAX_SIZE, + }); + + private static readonly nftContentCache = new LRUCache({ + max: Constants.CACHE_MAX_SIZE, + }); + + constructor( + private readonly commonService: CommonService, + private readonly tokenService: TokenService, + @InjectRepository(TxOutEntity) + private readonly txOutRepository: Repository, + @InjectRepository(NftInfoEntity) + private readonly nftInfoRepository: Repository, + @InjectRepository(TokenInfoEntity) + private readonly tokenInfoRepository: Repository, + ) {} + + async getCollectionContent( + collectionIdOrAddr: string, + ): Promise { + const key = `${collectionIdOrAddr}`; + let cached = CollectionService.nftContentCache.get(key); + if (!cached) { + const collectionInfo = + await this.tokenService.getTokenInfoByTokenIdOrTokenAddress( + collectionIdOrAddr, + TokenTypeScope.NonFungible, + ); + if (collectionInfo) { + const collectionContent = await this.tokenInfoRepository.findOne({ + select: [ + 'revealHeight', + 'contentType', + 'contentEncoding', + 'contentRaw', + ], + where: { tokenId: collectionInfo.tokenId }, + }); + if (collectionContent) { + cached = { + type: collectionContent.contentType, + encoding: collectionContent.contentEncoding, + raw: collectionContent.contentRaw, + }; + const lastProcessedHeight = + await this.commonService.getLastProcessedBlockHeight(); + if ( + lastProcessedHeight !== null && + lastProcessedHeight - collectionContent.revealHeight >= + Constants.CACHE_AFTER_N_BLOCKS + ) { + CollectionService.nftContentCache.set(key, cached); + } + } + } + } + return cached; + } + + async getNftInfo(collectionIdOrAddr: string, localId: bigint) { + const key = `${collectionIdOrAddr}_${localId}`; + let cached = CollectionService.nftInfoCache.get(key); + if (!cached) { + const collectionInfo = + await this.tokenService.getTokenInfoByTokenIdOrTokenAddress( + collectionIdOrAddr, + TokenTypeScope.NonFungible, + ); + if (collectionInfo) { + const nftInfo = await this.nftInfoRepository.findOne({ + select: [ + 'collectionId', + 'localId', + 'mintTxid', + 'mintHeight', + 'commitTxid', + 'metadata', + ], + where: { collectionId: collectionInfo.tokenId, localId }, + }); + if (nftInfo) { + const lastProcessedHeight = + await this.commonService.getLastProcessedBlockHeight(); + if ( + lastProcessedHeight !== null && + lastProcessedHeight - nftInfo.mintHeight >= + Constants.CACHE_AFTER_N_BLOCKS + ) { + CollectionService.nftInfoCache.set(key, nftInfo); + } + } + cached = nftInfo; + } + } + return cached; + } + + async getNftContent( + collectionIdOrAddr: string, + localId: bigint, + ): Promise { + const key = `${collectionIdOrAddr}_${localId}`; + let cached = CollectionService.nftContentCache.get(key); + if (!cached) { + const collectionInfo = + await this.tokenService.getTokenInfoByTokenIdOrTokenAddress( + collectionIdOrAddr, + TokenTypeScope.NonFungible, + ); + if (collectionInfo) { + const nftContent = await this.nftInfoRepository.findOne({ + select: [ + 'mintHeight', + 'contentType', + 'contentEncoding', + 'contentRaw', + ], + where: { collectionId: collectionInfo.tokenId, localId }, + }); + if (nftContent) { + cached = { + type: nftContent.contentType, + encoding: nftContent.contentEncoding, + raw: nftContent.contentRaw, + }; + const lastProcessedHeight = + await this.commonService.getLastProcessedBlockHeight(); + if ( + lastProcessedHeight !== null && + lastProcessedHeight - nftContent.mintHeight >= + Constants.CACHE_AFTER_N_BLOCKS + ) { + CollectionService.nftContentCache.set(key, cached); + } + } + } + } + return cached; + } + + async getNftUtxo(collectionIdOrAddr: string, localId: bigint) { + const lastProcessedHeight = + await this.commonService.getLastProcessedBlockHeight(); + const collectionInfo = + await this.tokenService.getTokenInfoByTokenIdOrTokenAddress( + collectionIdOrAddr, + TokenTypeScope.NonFungible, + ); + let utxos = []; + if (collectionInfo && collectionInfo.tokenPubKey) { + const where = { + xOnlyPubKey: collectionInfo.tokenPubKey, + tokenAmount: localId, + spendTxid: IsNull(), + blockHeight: LessThanOrEqual(lastProcessedHeight), + }; + utxos = await this.txOutRepository.find({ + where, + take: 1, + }); + } + const renderedUtxos = await this.tokenService.renderUtxos( + utxos, + collectionInfo, + ); + const utxo = renderedUtxos.length > 0 ? renderedUtxos[0] : null; + return { + utxo, + trackerBlockHeight: lastProcessedHeight, + }; + } +} diff --git a/packages/tracker/src/routes/minter/minter.controller.ts b/packages/tracker/src/routes/minter/minter.controller.ts index 5042c810..8fd95a31 100644 --- a/packages/tracker/src/routes/minter/minter.controller.ts +++ b/packages/tracker/src/routes/minter/minter.controller.ts @@ -30,8 +30,8 @@ export class MinterController { }) async getMinterUtxos( @Param('tokenIdOrTokenAddr') tokenIdOrTokenAddr: string, - @Query('offset') offset: number, - @Query('limit') limit: number, + @Query('offset') offset?: number, + @Query('limit') limit?: number, ) { try { const utxos = await this.minterService.getMinterUtxos( diff --git a/packages/tracker/src/routes/minter/minter.service.ts b/packages/tracker/src/routes/minter/minter.service.ts index 6538e6f5..119e319f 100644 --- a/packages/tracker/src/routes/minter/minter.service.ts +++ b/packages/tracker/src/routes/minter/minter.service.ts @@ -5,6 +5,7 @@ import { TxOutEntity } from '../../entities/txOut.entity'; import { IsNull, LessThanOrEqual, Repository } from 'typeorm'; import { Constants } from '../../common/constants'; import { CommonService } from '../../services/common/common.service'; +import { TokenTypeScope } from '../../common/types'; @Injectable() export class MinterService { @@ -17,8 +18,8 @@ export class MinterService { async getMinterUtxos( tokenIdOrTokenAddr: string, - offset: number, - limit: number, + offset?: number, + limit?: number, ) { const utxos = await this.queryMinterUtxos( tokenIdOrTokenAddr, @@ -42,14 +43,15 @@ export class MinterService { async queryMinterUtxos( tokenIdOrTokenAddr: string, isCountQuery: boolean = false, - offset: number = null, - limit: number = null, + offset: number | null = null, + limit: number | null = null, ) { const lastProcessedHeight = await this.commonService.getLastProcessedBlockHeight(); const tokenInfo = await this.tokenService.getTokenInfoByTokenIdOrTokenAddress( tokenIdOrTokenAddr, + TokenTypeScope.All, ); let count = 0; let utxos = []; diff --git a/packages/tracker/src/routes/token/token.controller.ts b/packages/tracker/src/routes/token/token.controller.ts index 4e5c52dc..d142bd4c 100644 --- a/packages/tracker/src/routes/token/token.controller.ts +++ b/packages/tracker/src/routes/token/token.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; import { TokenService } from './token.service'; import { okResponse, errorResponse } from '../../common/utils'; import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { TokenTypeScope } from '../../common/types'; @Controller('tokens') export class TokenController { @@ -21,6 +22,7 @@ export class TokenController { const tokenInfo = await this.tokenService.getTokenInfoByTokenIdOrTokenAddress( tokenIdOrTokenAddr, + TokenTypeScope.Fungible, ); return okResponse(tokenInfo); } catch (e) { @@ -28,7 +30,7 @@ export class TokenController { } } - @Get(':tokenIdOrTokenAddr/addresses/:ownerAddr/utxos') + @Get(':tokenIdOrTokenAddr/addresses/:ownerAddrOrPkh/utxos') @ApiTags('token') @ApiOperation({ summary: 'Get token utxos by owner address' }) @ApiParam({ @@ -38,10 +40,10 @@ export class TokenController { description: 'token id or token address', }) @ApiParam({ - name: 'ownerAddr', + name: 'ownerAddrOrPkh', required: true, type: String, - description: 'token owner address', + description: 'token owner address or public key hash', }) @ApiQuery({ name: 'offset', @@ -57,14 +59,15 @@ export class TokenController { }) async getTokenUtxosByOwnerAddress( @Param('tokenIdOrTokenAddr') tokenIdOrTokenAddr: string, - @Param('ownerAddr') ownerAddr: string, - @Query('offset') offset: number, - @Query('limit') limit: number, + @Param('ownerAddrOrPkh') ownerAddrOrPkh: string, + @Query('offset') offset?: number, + @Query('limit') limit?: number, ) { try { const utxos = await this.tokenService.getTokenUtxosByOwnerAddress( tokenIdOrTokenAddr, - ownerAddr, + TokenTypeScope.Fungible, + ownerAddrOrPkh, offset, limit, ); @@ -74,7 +77,7 @@ export class TokenController { } } - @Get(':tokenIdOrTokenAddr/addresses/:ownerAddr/balance') + @Get(':tokenIdOrTokenAddr/addresses/:ownerAddrOrPkh/balance') @ApiTags('token') @ApiOperation({ summary: 'Get token balance by owner address' }) @ApiParam({ @@ -84,19 +87,20 @@ export class TokenController { description: 'token id or token address', }) @ApiParam({ - name: 'ownerAddr', + name: 'ownerAddrOrPkh', required: true, type: String, - description: 'token owner address', + description: 'token owner address or public key hash', }) async getTokenBalanceByOwnerAddress( @Param('tokenIdOrTokenAddr') tokenIdOrTokenAddr: string, - @Param('ownerAddr') ownerAddr: string, + @Param('ownerAddrOrPkh') ownerAddrOrPkh: string, ) { try { const balance = await this.tokenService.getTokenBalanceByOwnerAddress( tokenIdOrTokenAddr, - ownerAddr, + TokenTypeScope.Fungible, + ownerAddrOrPkh, ); return okResponse(balance); } catch (e) { diff --git a/packages/tracker/src/routes/token/token.service.ts b/packages/tracker/src/routes/token/token.service.ts index 222fca4e..be4e36bd 100644 --- a/packages/tracker/src/routes/token/token.service.ts +++ b/packages/tracker/src/routes/token/token.service.ts @@ -1,7 +1,13 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { TokenInfoEntity } from '../../entities/tokenInfo.entity'; -import { IsNull, LessThanOrEqual, Repository } from 'typeorm'; +import { + IsNull, + LessThanOrEqual, + Repository, + MoreThanOrEqual, + LessThan, +} from 'typeorm'; import { addressToXOnlyPubKey, ownerAddressToPubKeyHash, @@ -12,6 +18,7 @@ import { Constants } from '../../common/constants'; import { LRUCache } from 'lru-cache'; import { TxEntity } from '../../entities/tx.entity'; import { CommonService } from '../../services/common/common.service'; +import { TokenTypeScope } from '../../common/types'; @Injectable() export class TokenService { @@ -36,10 +43,13 @@ export class TokenService { private readonly txRepository: Repository, ) {} - async getTokenInfoByTokenIdOrTokenAddress(tokenIdOrTokenAddr: string) { + async getTokenInfoByTokenIdOrTokenAddress( + tokenIdOrTokenAddr: string, + scope: TokenTypeScope, + ) { let cached = TokenService.tokenInfoCache.get(tokenIdOrTokenAddr); if (!cached) { - let where; + let where: object; if (tokenIdOrTokenAddr.includes('_')) { where = { tokenId: tokenIdOrTokenAddr }; } else { @@ -49,7 +59,25 @@ export class TokenService { } where = { tokenPubKey }; } + if (scope === TokenTypeScope.Fungible) { + where = Object.assign(where, { decimals: MoreThanOrEqual(0) }); + } else if (scope === TokenTypeScope.NonFungible) { + where = Object.assign(where, { decimals: LessThan(0) }); + } const tokenInfo = await this.tokenInfoRepository.findOne({ + select: [ + 'tokenId', + 'revealTxid', + 'revealHeight', + 'genesisTxid', + 'name', + 'symbol', + 'decimals', + 'rawInfo', + 'minterPubKey', + 'tokenPubKey', + 'firstMintHeight', + ], where, }); if (tokenInfo && tokenInfo.tokenPubKey) { @@ -58,16 +86,27 @@ export class TokenService { if ( lastProcessedHeight !== null && lastProcessedHeight - tokenInfo.revealHeight >= - Constants.TOKEN_INFO_CACHE_BLOCKS_THRESHOLD + Constants.CACHE_AFTER_N_BLOCKS ) { TokenService.tokenInfoCache.set(tokenIdOrTokenAddr, tokenInfo); } } cached = tokenInfo; + } else { + if (cached.decimals < 0 && scope === TokenTypeScope.Fungible) { + cached = null; + } else if (cached.decimals >= 0 && scope === TokenTypeScope.NonFungible) { + cached = null; + } } return this.renderTokenInfo(cached); } + async getTokenInfoByTokenPubKey(tokenPubKey: string, scope: TokenTypeScope) { + const tokenAddr = xOnlyPubKeyToAddress(tokenPubKey); + return this.getTokenInfoByTokenIdOrTokenAddress(tokenAddr, scope); + } + renderTokenInfo(tokenInfo: TokenInfoEntity) { if (!tokenInfo) { return null; @@ -80,26 +119,27 @@ export class TokenService { tokenInfo, ); delete rendered.rawInfo; - delete rendered.createdAt; - delete rendered.updatedAt; return rendered; } async getTokenUtxosByOwnerAddress( tokenIdOrTokenAddr: string, - ownerAddr: string, - offset: number, - limit: number, + scope: TokenTypeScope, + ownerAddrOrPkh: string, + offset?: number, + limit?: number, ) { const lastProcessedHeight = await this.commonService.getLastProcessedBlockHeight(); - const tokenInfo = - await this.getTokenInfoByTokenIdOrTokenAddress(tokenIdOrTokenAddr); + const tokenInfo = await this.getTokenInfoByTokenIdOrTokenAddress( + tokenIdOrTokenAddr, + scope, + ); let utxos = []; if (tokenInfo) { utxos = await this.queryTokenUtxosByOwnerAddress( lastProcessedHeight, - ownerAddr, + ownerAddrOrPkh, tokenInfo, offset || Constants.QUERY_PAGING_DEFAULT_OFFSET, Math.min( @@ -109,30 +149,33 @@ export class TokenService { ); } return { - utxos: await this.renderUtxos(utxos), + utxos: await this.renderUtxos(utxos, tokenInfo), trackerBlockHeight: lastProcessedHeight, }; } async getTokenBalanceByOwnerAddress( tokenIdOrTokenAddr: string, - ownerAddr: string, + scope: TokenTypeScope, + ownerAddrOrPkh: string, ) { const lastProcessedHeight = await this.commonService.getLastProcessedBlockHeight(); - const tokenInfo = - await this.getTokenInfoByTokenIdOrTokenAddress(tokenIdOrTokenAddr); + const tokenInfo = await this.getTokenInfoByTokenIdOrTokenAddress( + tokenIdOrTokenAddr, + scope, + ); let utxos = []; if (tokenInfo) { utxos = await this.queryTokenUtxosByOwnerAddress( lastProcessedHeight, - ownerAddr, + ownerAddrOrPkh, tokenInfo, ); } let confirmed = '0'; if (tokenInfo?.tokenPubKey) { - const tokenBalances = this.groupTokenBalances(utxos); + const tokenBalances = await this.groupTokenBalances(utxos); confirmed = tokenBalances[tokenInfo.tokenPubKey]?.toString() || '0'; } return { @@ -144,12 +187,15 @@ export class TokenService { async queryTokenUtxosByOwnerAddress( lastProcessedHeight: number, - ownerAddr: string, - tokenInfo: TokenInfoEntity = null, - offset: number = null, - limit: number = null, + ownerAddrOrPkh: string, + tokenInfo: TokenInfoEntity | null = null, + offset: number | null = null, + limit: number | null = null, ) { - const ownerPubKeyHash = ownerAddressToPubKeyHash(ownerAddr); + const ownerPubKeyHash = + ownerAddrOrPkh.length === Constants.PUBKEY_HASH_BYTES * 2 + ? ownerAddrOrPkh + : ownerAddressToPubKeyHash(ownerAddrOrPkh); if ( lastProcessedHeight === null || (tokenInfo && !tokenInfo.tokenPubKey) || @@ -191,7 +237,10 @@ export class TokenService { return cached; } - async renderUtxos(utxos: TxOutEntity[]) { + /** + * render token utxos when passing tokenInfo, otherwise render minter utxos + */ + async renderUtxos(utxos: TxOutEntity[], tokenInfo?: TokenInfoEntity) { const renderedUtxos = []; for (const utxo of utxos) { const txoStateHashes = await this.queryStateHashes(utxo.txid); @@ -205,12 +254,22 @@ export class TokenService { txoStateHashes, }; if (utxo.ownerPubKeyHash !== null && utxo.tokenAmount !== null) { - Object.assign(renderedUtxo, { - state: { - address: utxo.ownerPubKeyHash, - amount: utxo.tokenAmount, - }, - }); + Object.assign( + renderedUtxo, + tokenInfo && tokenInfo.decimals >= 0 + ? { + state: { + address: utxo.ownerPubKeyHash, + amount: utxo.tokenAmount, + }, + } + : { + state: { + address: utxo.ownerPubKeyHash, + localId: utxo.tokenAmount, + }, + }, + ); } renderedUtxos.push(renderedUtxo); } @@ -221,11 +280,17 @@ export class TokenService { * @param utxos utxos with the same owner address * @returns token balances grouped by xOnlyPubKey */ - groupTokenBalances(utxos: TxOutEntity[]) { + async groupTokenBalances(utxos: TxOutEntity[]) { const balances = {}; for (const utxo of utxos) { - balances[utxo.xOnlyPubKey] = - (balances[utxo.xOnlyPubKey] || 0n) + BigInt(utxo.tokenAmount); + const tokenInfo = await this.getTokenInfoByTokenPubKey( + utxo.xOnlyPubKey, + TokenTypeScope.All, + ); + if (tokenInfo) { + const acc = tokenInfo.decimals >= 0 ? BigInt(utxo.tokenAmount) : 1n; + balances[utxo.xOnlyPubKey] = (balances[utxo.xOnlyPubKey] || 0n) + acc; + } } return balances; } diff --git a/packages/tracker/src/services/tx/tx.service.ts b/packages/tracker/src/services/tx/tx.service.ts index ff4a2d6d..7f18346f 100644 --- a/packages/tracker/src/services/tx/tx.service.ts +++ b/packages/tracker/src/services/tx/tx.service.ts @@ -17,11 +17,20 @@ import { TxOutEntity } from '../../entities/txOut.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Constants } from '../../common/constants'; import { TokenInfoEntity } from '../../entities/tokenInfo.entity'; +import { NftInfoEntity } from '../../entities/nftInfo.entity'; import { CatTxError } from '../../common/exceptions'; -import { parseTokenInfo, TaprootPayment } from '../../common/utils'; -import { BlockHeader, TokenInfo } from '../../common/types'; +import { parseTokenInfoEnvelope } from '../../common/utils'; +import { + BlockHeader, + EnvelopeMarker, + TaprootPayment, + TokenInfoEnvelope, +} from '../../common/types'; import { TokenMintEntity } from '../../entities/tokenMint.entity'; -import { getGuardContractInfo } from '@cat-protocol/cat-smartcontracts'; +import { + getGuardContractInfo, + getNftGuardContractInfo, +} from '@cat-protocol/cat-smartcontracts'; import { LRUCache } from 'lru-cache'; import { CommonService } from '../common/common.service'; import { TxOutArchiveEntity } from 'src/entities/txOutArchive.entity'; @@ -31,8 +40,11 @@ import { Cron } from '@nestjs/schedule'; export class TxService { private readonly logger = new Logger(TxService.name); - private readonly GUARD_PUBKEY: string; - private readonly TRANSFER_GUARD_SCRIPT_HASH: string; + private readonly FT_GUARD_PUBKEY: string; + private readonly FT_TRANSFER_GUARD_SCRIPT_HASH: string; + + private readonly NFT_GUARD_PUBKEY: string; + private readonly NFT_TRANSFER_GUARD_SCRIPT_HASH: string; private static readonly taprootPaymentCache = new LRUCache< string, @@ -56,13 +68,22 @@ export class TxService { @InjectRepository(TxEntity) private txEntityRepository: Repository, ) { - const guardContractInfo = getGuardContractInfo(); - this.GUARD_PUBKEY = guardContractInfo.tpubkey; - this.TRANSFER_GUARD_SCRIPT_HASH = - guardContractInfo.contractTaprootMap.transfer.contractScriptHash; - this.logger.log(`guard xOnlyPubKey = ${this.GUARD_PUBKEY}`); + const tokenGuardContractInfo = getGuardContractInfo(); + this.FT_GUARD_PUBKEY = tokenGuardContractInfo.tpubkey; + this.FT_TRANSFER_GUARD_SCRIPT_HASH = + tokenGuardContractInfo.contractTaprootMap.transfer.contractScriptHash; + this.logger.log(`token guard xOnlyPubKey = ${this.FT_GUARD_PUBKEY}`); + this.logger.log( + `token guard transferScriptHash = ${this.FT_TRANSFER_GUARD_SCRIPT_HASH}`, + ); + + const nftGuardContractInfo = getNftGuardContractInfo(); + this.NFT_GUARD_PUBKEY = nftGuardContractInfo.tpubkey; + this.NFT_TRANSFER_GUARD_SCRIPT_HASH = + nftGuardContractInfo.contractTaprootMap.transfer.contractScriptHash; + this.logger.log(`nft guard xOnlyPubKey = ${this.NFT_GUARD_PUBKEY}`); this.logger.log( - `guard transferScriptHash = ${this.TRANSFER_GUARD_SCRIPT_HASH}`, + `nft guard transferScriptHash = ${this.NFT_TRANSFER_GUARD_SCRIPT_HASH}`, ); } @@ -120,6 +141,7 @@ export class TxService { queryRunner.manager, promises, tx, + payIns, payOuts, minterInput, tokenInfo, @@ -218,7 +240,10 @@ export class TxService { */ private searchGuardOutputs(payOuts: TaprootPayment[]): boolean { for (const payOut of payOuts) { - if (this.GUARD_PUBKEY === payOut?.pubkey?.toString('hex')) { + if ( + this.FT_GUARD_PUBKEY === payOut?.pubkey?.toString('hex') || + this.NFT_GUARD_PUBKEY === payOut?.pubkey?.toString('hex') + ) { return true; } } @@ -231,7 +256,10 @@ export class TxService { */ private searchGuardInputs(payIns: TaprootPayment[]): TaprootPayment[] { return payIns.filter((payIn) => { - return this.GUARD_PUBKEY === payIn?.pubkey?.toString('hex'); + return ( + this.FT_GUARD_PUBKEY === payIn?.pubkey?.toString('hex') || + this.NFT_GUARD_PUBKEY === payIn?.pubkey?.toString('hex') + ); }); } @@ -273,6 +301,17 @@ export class TxService { let tokenInfo = TxService.tokenInfoCache.get(minterPubKey); if (!tokenInfo) { tokenInfo = await this.tokenInfoEntityRepository.findOne({ + select: [ + 'tokenId', + 'revealTxid', + 'revealHeight', + 'genesisTxid', + 'name', + 'symbol', + 'decimals', + 'minterPubKey', + 'tokenPubKey', + ], where: { minterPubKey }, }); if (tokenInfo && tokenInfo.tokenPubKey) { @@ -281,7 +320,7 @@ export class TxService { if ( lastProcessedHeight !== null && lastProcessedHeight - tokenInfo.revealHeight >= - Constants.TOKEN_INFO_CACHE_BLOCKS_THRESHOLD + Constants.CACHE_AFTER_N_BLOCKS ) { TxService.tokenInfoCache.set(minterPubKey, tokenInfo); } @@ -299,13 +338,17 @@ export class TxService { blockHeader: BlockHeader, ) { // commit input - const { inputIndex: commitInputIndex, tokenInfo } = + const { inputIndex: commitInputIndex, envelope } = this.searchRevealTxCommitInput(payIns); const commitInput = payIns[commitInputIndex]; const genesisTxid = Buffer.from(tx.ins[commitInputIndex].hash) .reverse() .toString('hex'); const tokenId = `${genesisTxid}_${tx.ins[commitInputIndex].index}`; + const { + marker, + data: { metadata, content }, + } = envelope; // state hashes const stateHashes = commitInput.witness.slice( Constants.CONTRACT_INPUT_WITNESS_STATE_HASHES_OFFSET, @@ -322,10 +365,13 @@ export class TxService { revealTxid: tx.getId(), revealHeight: blockHeader.height, genesisTxid, - name: tokenInfo.name, - symbol: tokenInfo.symbol, - decimals: tokenInfo.decimals, - rawInfo: tokenInfo, + name: metadata['name'], + symbol: metadata['symbol'], + decimals: marker === EnvelopeMarker.Token ? metadata['decimals'] : -1, + rawInfo: metadata, + contentType: content?.type, + contentEncoding: content?.encoding, + contentRaw: content?.raw, minterPubKey, }), ); @@ -353,20 +399,24 @@ export class TxService { * If there are multiple commit inputs, throw an error. * If there is no commit input, throw an error. */ - private searchRevealTxCommitInput(payIn: TaprootPayment[]): { + private searchRevealTxCommitInput(payIns: TaprootPayment[]): { inputIndex: number; - tokenInfo: TokenInfo; + envelope: TokenInfoEnvelope; } { let commit = null; - for (let i = 0; i < payIn.length; i++) { + for (let i = 0; i < payIns.length; i++) { if ( - payIn[i] && - payIn[i].witness.length >= Constants.COMMIT_INPUT_WITNESS_MIN_SIZE + payIns[i] && + payIns[i].witness.length >= Constants.COMMIT_INPUT_WITNESS_MIN_SIZE ) { try { // parse token info from commit redeem script - const tokenInfo = parseTokenInfo(payIn[i].redeemScript); - if (tokenInfo) { + const envelope = parseTokenInfoEnvelope(payIns[i].redeemScript); + if ( + envelope && + (envelope.marker === EnvelopeMarker.Token || + envelope.marker === EnvelopeMarker.Collection) + ) { // token info is valid here if (commit) { throw new CatTxError( @@ -375,7 +425,7 @@ export class TxService { } commit = { inputIndex: i, - tokenInfo, + envelope, }; } } catch (e) { @@ -419,6 +469,7 @@ export class TxService { manager: EntityManager, promises: Promise[], tx: Transaction, + payIns: TaprootPayment[], payOuts: TaprootPayment[], minterInput: TaprootPayment, tokenInfo: TokenInfoEntity, @@ -435,43 +486,63 @@ export class TxService { this.validateStateHashes(stateHashes); // ownerPubKeyHash - if ( - minterInput.witness[Constants.MINTER_INPUT_WITNESS_ADDR_OFFSET].length !== - Constants.PUBKEY_HASH_BYTES - ) { + const pkh = minterInput.witness[Constants.MINTER_INPUT_WITNESS_ADDR_OFFSET]; + if (pkh.length !== Constants.PUBKEY_HASH_BYTES) { throw new CatTxError( 'invalid mint tx, invalid byte length of owner pubkey hash', ); } - const ownerPubKeyHash = - minterInput.witness[Constants.MINTER_INPUT_WITNESS_ADDR_OFFSET].toString( - 'hex', - ); + const ownerPubKeyHash = pkh.toString('hex'); + // tokenAmount - if ( - minterInput.witness[Constants.MINTER_INPUT_WITNESS_AMOUNT_OFFSET].length > - Constants.TOKEN_AMOUNT_MAX_BYTES - ) { + const amount = + minterInput.witness[Constants.MINTER_INPUT_WITNESS_AMOUNT_OFFSET]; + if (amount.length > Constants.TOKEN_AMOUNT_MAX_BYTES) { throw new CatTxError( 'invalid mint tx, invalid byte length of token amount', ); } - const tokenAmount = BigInt( - minterInput.witness[ - Constants.MINTER_INPUT_WITNESS_AMOUNT_OFFSET - ].readIntLE( - 0, - minterInput.witness[Constants.MINTER_INPUT_WITNESS_AMOUNT_OFFSET] - .length, - ), - ); - if (tokenAmount <= 0n) { + const tokenAmount = + amount.length === 0 ? 0n : BigInt(amount.readIntLE(0, amount.length)); + if (tokenAmount < 0n) { + throw new CatTxError( + 'invalid mint tx, token amount should be non-negative', + ); + } + if (tokenAmount === 0n && tokenInfo.decimals >= 0) { throw new CatTxError('invalid mint tx, token amount should be positive'); } + // token output const { tokenPubKey, outputIndex: tokenOutputIndex } = this.searchMintTxTokenOutput(payOuts, tokenInfo); + // save nft info + if (tokenInfo.decimals < 0) { + const commitInput = this.searchMintTxCommitInput(payIns); + if (commitInput) { + const { inputIndex: commitInuptIndex, envelope } = commitInput; + const commitTxid = Buffer.from(tx.ins[commitInuptIndex].hash) + .reverse() + .toString('hex'); + const { + data: { metadata, content }, + } = envelope; + promises.push( + manager.save(NftInfoEntity, { + collectionId: tokenInfo.tokenId, + localId: tokenAmount, + mintTxid: tx.getId(), + mintHeight: blockHeader.height, + commitTxid, + metadata: metadata, + contentType: content?.type, + contentEncoding: content?.encoding, + contentRaw: content?.raw, + }), + ); + } + } // update token info when first mint if (tokenInfo.tokenPubKey === null) { promises.push( @@ -523,6 +594,7 @@ export class TxService { .filter((out) => out !== null), ), ); + return stateHashes; } @@ -597,6 +669,31 @@ export class TxService { return tokenOutput; } + /** + * try to parse the nft info in mint tx inputs + */ + private searchMintTxCommitInput(payIns: TaprootPayment[]): { + inputIndex: number; + envelope: TokenInfoEnvelope; + } | null { + for (let i = 0; i < payIns.length; i++) { + if (payIns[i]) { + try { + const envelope = parseTokenInfoEnvelope(payIns[i].redeemScript); + if (envelope && envelope.marker === EnvelopeMarker.NFT) { + return { + inputIndex: i, + envelope, + }; + } + } catch (e) { + this.logger.error(`search commit in mint tx error, ${e.message}`); + } + } + } + return null; + } + private async processTransferTx( manager: EntityManager, promises: Promise[], @@ -618,7 +715,10 @@ export class TxService { const scriptHash = crypto .hash160(guardInput?.redeemScript || Buffer.alloc(0)) .toString('hex'); - if (scriptHash === this.TRANSFER_GUARD_SCRIPT_HASH) { + if ( + scriptHash === this.FT_TRANSFER_GUARD_SCRIPT_HASH || + scriptHash === this.NFT_TRANSFER_GUARD_SCRIPT_HASH + ) { const tokenOutputs = this.parseTokenOutputs(guardInput); // save tx outputs promises.push( @@ -666,9 +766,10 @@ export class TxService { for (let i = 0; i < Constants.CONTRACT_OUTPUT_MAX_COUNT; i++) { if (masks[i].toString('hex') !== '') { const ownerPubKeyHash = ownerPubKeyHashes[i].toString('hex'); - const tokenAmount = BigInt( - tokenAmounts[i].readIntLE(0, tokenAmounts[i].length), - ); + const tokenAmount = + tokenAmounts[i].length === 0 + ? 0n + : BigInt(tokenAmounts[i].readIntLE(0, tokenAmounts[i].length)); tokenOutputs.set(i + 1, { ownerPubKeyHash, tokenAmount, @@ -761,6 +862,9 @@ export class TxService { manager.delete(TokenInfoEntity, { revealHeight: MoreThanOrEqual(height), }), + manager.delete(NftInfoEntity, { + mintHeight: MoreThanOrEqual(height), + }), manager.update( TokenInfoEntity, { firstMintHeight: MoreThanOrEqual(height) }, @@ -812,6 +916,7 @@ export class TxService { @Cron('* * * * *') private async archiveTxOuts() { + const startTime = Date.now(); const lastProcessedHeight = await this.commonService.getLastProcessedBlockHeight(); if (lastProcessedHeight === null) { @@ -842,6 +947,8 @@ export class TxService { ), ]); }); - this.logger.log(`archived ${txOuts.length} tx outputs`); + this.logger.log( + `archived ${txOuts.length} outs in ${Math.ceil(Date.now() - startTime)} ms`, + ); } } diff --git a/packages/tracker/test/parser.spec.ts b/packages/tracker/test/parser.spec.ts index ef1e6520..d0526372 100644 --- a/packages/tracker/test/parser.spec.ts +++ b/packages/tracker/test/parser.spec.ts @@ -1,5 +1,6 @@ import { script } from 'bitcoinjs-lib'; -import { parseTokenInfo } from '../src/common/utils'; +import { parseTokenInfoEnvelope } from '../src/common/utils'; +import { EnvelopeMarker } from '../src//common/types'; describe('parsing token info from redeem script', () => { const correctCbor = @@ -10,34 +11,34 @@ describe('parsing token info from redeem script', () => { const invalidCbor = incompleteCbor.substring(2); it('should throw when parsing invalid script', () => { - expect(() => parseTokenInfo(Buffer.from('0201', 'hex'))).toThrow( - 'parse token info error', + expect(() => parseTokenInfoEnvelope(Buffer.from('0201', 'hex'))).toThrow( + 'parse token info envelope error', ); }); it('should return null when script is empty', () => { - expect(parseTokenInfo(null)).toBeNull(); - expect(parseTokenInfo(Buffer.alloc(0))).toBeNull(); + expect(parseTokenInfoEnvelope(null)).toBeNull(); + expect(parseTokenInfoEnvelope(Buffer.alloc(0))).toBeNull(); }); it('should return null when script missing the envelope', () => { const scriptHex = script.fromASM(`${incompleteCbor}`).toString('hex'); - expect(parseTokenInfo(Buffer.from(scriptHex, 'hex'))).toBeNull(); + expect(parseTokenInfoEnvelope(Buffer.from(scriptHex, 'hex'))).toBeNull(); }); it('should return null when token info missing fields', () => { const scriptHex = script .fromASM(`OP_0 OP_IF 636174 OP_1 ${incompleteCbor} OP_ENDIF`) .toString('hex'); - expect(parseTokenInfo(Buffer.from(scriptHex, 'hex'))).toBeNull(); + expect(parseTokenInfoEnvelope(Buffer.from(scriptHex, 'hex'))).toBeNull(); }); it('should throw when parsing incorrect cbor encoding', () => { const scriptHex = script .fromASM(`OP_0 OP_IF 636174 OP_1 ${invalidCbor} OP_ENDIF`) .toString('hex'); - expect(() => parseTokenInfo(Buffer.from(scriptHex, 'hex'))).toThrow( - 'parse token info error', + expect(() => parseTokenInfoEnvelope(Buffer.from(scriptHex, 'hex'))).toThrow( + 'parse token info envelope error', ); }); @@ -45,7 +46,10 @@ describe('parsing token info from redeem script', () => { const scriptHex = script .fromASM(`OP_0 OP_IF 636174 OP_1 ${correctCbor} OP_ENDIF`) .toString('hex'); - expect(parseTokenInfo(Buffer.from(scriptHex, 'hex'))).toEqual(correctInfo); + expect(parseTokenInfoEnvelope(Buffer.from(scriptHex, 'hex'))).toEqual({ + marker: EnvelopeMarker.Token, + data: { metadata: correctInfo }, + }); }); it('should pass when token info consists of multiple pushdata', () => { @@ -55,6 +59,21 @@ describe('parsing token info from redeem script', () => { const scriptHex = script .fromASM(`OP_0 OP_IF 636174 OP_1 ${pushdata1} ${pushdaat2} OP_ENDIF`) .toString('hex'); - expect(parseTokenInfo(Buffer.from(scriptHex, 'hex'))).toEqual(correctInfo); + expect(parseTokenInfoEnvelope(Buffer.from(scriptHex, 'hex'))).toEqual({ + marker: EnvelopeMarker.Token, + data: { metadata: correctInfo }, + }); + }); + + it('should pass when parsing correct collection info', () => { + const metadata = { name: 'hh', symbol: 'hh' }; + const cbor = 'a2646e616d656268686673796d626f6c626868'; + const scriptHex = script + .fromASM(`OP_0 OP_IF 636174 OP_2 OP_5 ${cbor} OP_ENDIF`) + .toString('hex'); + expect(parseTokenInfoEnvelope(Buffer.from(scriptHex, 'hex'))).toEqual({ + marker: EnvelopeMarker.Collection, + data: { metadata }, + }); }); }); diff --git a/yarn.lock b/yarn.lock index ef24c968..3c8d7efa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7084,7 +7084,7 @@ scrypt-ts-transpiler@^1.2.26: ts-patch "=3.0.1" typescript "~5.3.0" -scrypt-ts@latest: +scrypt-ts@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/scrypt-ts/-/scrypt-ts-1.4.0.tgz#d72b9aa4e28ba16d2b33e58e93c503f10d97f448" integrity sha512-mmdnJ0zDopUo44sfXbgUOlxBF1mXNJhL8lyTy77GgC/rez85vS1nzeLf9SuKe9/BZJDM/vu7rIu08mpMkS/8ag==