├── screenshot ├── log.jpg └── etherscan.png ├── config.js ├── package.json ├── LICENSE ├── README.md └── index.js /screenshot/log.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyeETH/collect-nft/HEAD/screenshot/log.jpg -------------------------------------------------------------------------------- /screenshot/etherscan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyeETH/collect-nft/HEAD/screenshot/etherscan.png -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | alchemyKey: "", // alchemy提供的ApiKey 3 | maxPriority: "2", // 最大优先费 4 | maxGasPrice: "30", // 最大Gas 5 | multipleGas: 1.2, // gaslimit倍数,默认1.2,小于1会导致发生out of gas失败,过高需要余额充足 [>=1] 6 | collectAddress: "", // 归集地址 7 | // 地址 8 | publicKey: [ 9 | "0x0000000000000000000000000000000000000000", 10 | "0x1111111111111111111111111111111111111111", 11 | ], 12 | // 私钥 13 | privateKey: [ 14 | "0x0000000000000000000000000000000000000000000000000000000000000000", 15 | "0x1111111111111111111111111111111111111111111111111111111111111111", 16 | 17 | ], 18 | // 账户别名 19 | nickName: [ 20 | "示例账户1", 21 | "示例账户2", 22 | ], 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collect-nft", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "bin": "./index.js", 6 | "scripts": { 7 | "pkg": "pkg . --out-path=dist/" 8 | }, 9 | "pkg": { 10 | "scripts": "" 11 | }, 12 | "author": "luoyeeth", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@alch/alchemy-web3": "^1.4.1", 16 | "axios": "^0.26.1", 17 | "dotenv": "^16.0.0", 18 | "moment": "^2.29.3" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/luoyeETH/collect-nft.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/luoyeETH/collect-nft/issues" 26 | }, 27 | "homepage": "https://github.com/luoyeETH/collect-nft#readme", 28 | "description": "" 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 luoye.eth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## collect-NFT 2 | 在本地批量归集多个钱包的nft到归集地址 3 | 同时支持按合约查询nft库存 4 | 5 | > 实际测试时发现,对钱包内余额为1的nft归集手续费略低于授权,对钱包内余额超过1的nft归集手续费会成倍增加,并不适用于使用脚本归集。 6 | 7 | 8 | ## 使用方法 9 | 10 | ### 配置config.js 11 | 1.`alchemyKey` [注册alchemy](https://alchemy.com/?r=TUwNjExMDY3MzM2M) 12 | 13 | 2.`maxPriority` 最大优先费 14 | 15 | 3.`maxGasPrice` 最大Gas费 16 | 17 | 4.`multipleGas` gaslimit倍数 18 | 19 | 5.`collectAddress` 归集的目标地址 20 | 21 | 6.`publicKey` 公钥/账户地址 22 | > 数组。["Address1","Address2"] 23 | 24 | 7.`privateKey` 私钥 25 | > 数组。["privateKey1","privateKey2"] 26 | 27 | 8.`nickName` 账户别名 28 | > 数组。["nickName1","nickName2"] 29 | 30 | **以上三组数据要一一对应并保持总数一致** 31 | 32 | ### 部署流程 33 | **Release中发布了exe版本,配置config.js后在Windows下可直接运行。** 34 | > linux和mac版本未测试 35 | 36 | 1.克隆或下载该项目到本地 37 | ``` 38 | git clone https://github.com/luoyeETH/collect-nft.git 39 | ``` 40 | 2.切换到对应目录,安装所需依赖包,同时建议安装forever进程管理模块 41 | ``` 42 | npm install 43 | npm install forever -g 44 | ``` 45 | 3.按照[配置文件](#配置configjs)配置config.js内各项参数 46 | 47 | 4.运行脚本 48 | ``` 49 | # 查询NFT库存 50 | # ContractAddress合约地址 51 | node index.js 52 | 53 | # 批量归集 54 | # ContractAddress合约地址 accounts归集的账户数目(填0全部归集) 55 | node index.js 56 | ``` 57 | 58 | 5.停止脚本 59 | ``` 60 | Ctrl + C 61 | ``` 62 | 63 | ## 运行截图 64 | 65 | 运行日志 66 | ![screenshot](screenshot/log.jpg) 67 | 68 | etherscan 69 | ![screenshot](screenshot/etherscan.png) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require(path.join(process.cwd(), "./config")) 3 | 4 | const API_URL = `https://eth-mainnet.alchemyapi.io/v2/${config.alchemyKey}` 5 | const PUBLIC_KEY_LIST = config.publicKey 6 | const PRIVATE_KEY_LIST = config.privateKey 7 | const NICK_NAME_LIST = config.nickName 8 | const MAX_PRIORITY_FEE_PER_GAS = config.maxPriority 9 | const MAX_FEE_PER_GAS = config.maxGasPrice 10 | const MULTIPLE_GAS = config.multipleGas 11 | const tokenId_LIST = [] 12 | 13 | const {createAlchemyWeb3} = require("@alch/alchemy-web3") 14 | const web3 = createAlchemyWeb3(API_URL) 15 | 16 | const moment = require("moment"); 17 | 18 | // 获取时间 19 | const getDate = () => { 20 | let date = moment(new Date()).utcOffset(8).format('YYYY-MM-DD HH:mm:ss.SSS'); 21 | return date 22 | } 23 | // 休眠函数 24 | function sleep(ms) { 25 | return new Promise(resolve => setTimeout(() => resolve(), ms)); 26 | }; 27 | 28 | const readline = require('readline').createInterface({ 29 | input: process.stdin, 30 | output: process.stdout 31 | }) 32 | 33 | 34 | // 初始化 35 | let date = getDate() 36 | console.log(`\n${date} 初始化完成\n`) 37 | // address-tokenIdList的map 38 | let addressTokenIdMap = new Map() 39 | 40 | //获取单地址NFT余额 并将16进制的tokenId存入Map 41 | async function getNFTBalance(contract, address) { 42 | const nfts = await web3.alchemy.getNfts({owner: address, contractAddresses: contract}) 43 | if (nfts.totalCount == 0) { 44 | return nfts.totalCount 45 | } 46 | console.log("\nnumber of NFTs found:", nfts.totalCount); 47 | let tokenIdList = [] 48 | for (const nft of nfts.ownedNfts) { 49 | // 16进制转10进制 50 | let tokenIdDec = parseInt(nft.id.tokenId, 16) 51 | console.log("token ID:", tokenIdDec); 52 | tokenId_LIST.push(tokenIdDec); 53 | // 保存16进制的tokenId到数组 54 | let tokenIdHex = nft.id.tokenId 55 | tokenIdList.push(tokenIdHex) 56 | } 57 | addressTokenIdMap.set(address, tokenIdList) 58 | let addressNftBalance = nfts.totalCount 59 | return addressNftBalance 60 | } 61 | 62 | // 循环获取所有地址的NFT余额 63 | const getAllNFTBalance = async (contract) => { 64 | let allNftBalance = 0 65 | for (let i = 0; i < PUBLIC_KEY_LIST.length; i++) { 66 | let address = PUBLIC_KEY_LIST[i] 67 | let name = NICK_NAME_LIST[i] 68 | contract = [contract] 69 | let nftBalance = await getNFTBalance(contract, address) 70 | if (nftBalance !== 0) { 71 | console.log(`${address} ${name} NFT balance: ${nftBalance}`) 72 | allNftBalance += nftBalance 73 | } 74 | } 75 | // 对数组排序 76 | tokenId_LIST.sort(function(a, b){return a-b}); 77 | console.log(tokenId_LIST) 78 | console.log(`NFT ${contract} \nALL Address balance: ${allNftBalance}`) 79 | return allNftBalance 80 | } 81 | 82 | // 通过accounts循环获取NFT余额 83 | const getNFTBalanceByAccounts = async (contract, accounts) => { 84 | let allNftBalance = 0 85 | for (let i = 0; i < accounts; i++) { 86 | let address = PUBLIC_KEY_LIST[i] 87 | let name = NICK_NAME_LIST[i] 88 | contract = [contract] 89 | let nftBalance = await getNFTBalance(contract, address) 90 | if (nftBalance !== 0) { 91 | console.log(`${address} ${name} NFT balance: ${nftBalance}`) 92 | allNftBalance += nftBalance 93 | } 94 | } 95 | console.log(`NFT ${contract} \nALL Address balance: ${allNftBalance}`) 96 | // 打印地址tokenId Map 97 | // for (let [key, value] of addressTokenIdMap) { 98 | // console.log(`${key} ${value}`) 99 | // } 100 | return allNftBalance 101 | } 102 | 103 | // 获取转移nft的十六进制数据(safeTransferFrom) 104 | const getInputData = async (fromAddress, toAddress, tokenId) => { 105 | fromAddress = fromAddress.toLowerCase() 106 | toAddress = toAddress.toLowerCase() 107 | let inputData = `0x42842e0e000000000000000000000000${fromAddress.slice(2)}000000000000000000000000${toAddress.slice(2)}${tokenId.slice(2)}` 108 | // console.log(`inputData: ${inputData}`); 109 | return inputData 110 | } 111 | 112 | // 转移NFT 113 | async function transferNFT(publicKey, privateKey, contract, toAddress, tokenId) { 114 | const nonce = await web3.eth.getTransactionCount(publicKey, "latest") 115 | inputData = await getInputData(publicKey, toAddress, tokenId) 116 | 117 | let tx = { 118 | from: publicKey, 119 | to: contract, 120 | nonce: nonce, 121 | value: 0, 122 | input: inputData, 123 | type: '0x2', 124 | maxPriorityFeePerGas: web3.utils.toHex(web3.utils.toWei(MAX_PRIORITY_FEE_PER_GAS, 'gwei')), 125 | maxFeePerGas: web3.utils.toHex(web3.utils.toWei(MAX_FEE_PER_GAS, 'gwei')), 126 | } 127 | // 计算gas和避开失败的交易 128 | let gas = await web3.eth.estimateGas(tx) 129 | tx.gas = parseInt(MULTIPLE_GAS * gas) 130 | 131 | // sign the transaction 132 | const signPromise = web3.eth.accounts.signTransaction(tx, privateKey) 133 | signPromise 134 | .then((signedTx) => { 135 | web3.eth.sendSignedTransaction( 136 | signedTx.rawTransaction, 137 | function (err, hash) { 138 | if (!err) { 139 | console.log("The hash of your transaction is: ", hash) 140 | } else { 141 | console.log("Something went wrong when submitting your transaction:", err) 142 | } 143 | } 144 | ) 145 | }) 146 | .catch((err) => { 147 | console.log("Promise failed:", err) 148 | }) 149 | } 150 | 151 | // 归集nft 152 | async function collectNFT(contract, toAddress, accounts) { 153 | console.log(`\n${date} 归集程序启动...\n`) 154 | if (accounts == 0) { 155 | accounts = PUBLIC_KEY_LIST.length; 156 | } else if (accounts > PUBLIC_KEY_LIST.length) { 157 | accounts = PUBLIC_KEY_LIST.length; 158 | } 159 | let allNftBalance = await getNFTBalanceByAccounts(contract, accounts) 160 | if (allNftBalance == 0) { 161 | console.log(`\n没有可以归集的NFT 请检查合约地址是否准确\n`) 162 | return 163 | } 164 | console.log(`\n开始归集...\n`) 165 | for (let i = 0; i < accounts; i++) { 166 | let address = PUBLIC_KEY_LIST[i] 167 | let name = NICK_NAME_LIST[i] 168 | let tokenIdList = addressTokenIdMap.get(address) 169 | if (tokenIdList == undefined) { 170 | console.log(`${address} ${name} 没有可以归集的NFT`) 171 | continue 172 | } 173 | for (let j = 0; j < tokenIdList.length; j++) { 174 | let tokenId = tokenIdList[j] 175 | await transferNFT(address, PRIVATE_KEY_LIST[i], contract, toAddress, tokenId).catch((err) => { 176 | console.log(`${address} ${name} 归集NFT失败 \n${err}`) 177 | }) 178 | if (j < tokenIdList.length - 1) { 179 | await sleep(60000); 180 | } 181 | } 182 | } 183 | await sleep(10000); 184 | console.log(`\n归集完成\n`) 185 | } 186 | 187 | const startRun = async () => { 188 | 189 | if (process.argv.length < 3) { 190 | console.log(`\n请输入合约地址和归集账户数\n`) 191 | const contractAddress = await new Promise(resolve => { 192 | readline.question("合约地址? ", resolve) 193 | }) 194 | console.log(); 195 | const accounts = await new Promise(resolve => { 196 | readline.question("归集账户数?(按回车则不归集) ", resolve) 197 | }) 198 | console.log(`\n合约地址: ${contractAddress}\n`); 199 | if (accounts == '') { 200 | console.log(`\n开始查询库存 \n`) 201 | await getAllNFTBalance(contractAddress) 202 | } else { 203 | console.log(`\n归集账户数 ${accounts}\n`) 204 | let toAddress = config.collectAddress; 205 | await collectNFT(contractAddress, toAddress, accounts); 206 | } 207 | } 208 | // 查询NFT库存 209 | else if (process.argv.length == 3) { 210 | args = process.argv.slice(2); 211 | let contractAddress = args[0]; 212 | await getAllNFTBalance(contractAddress) 213 | } 214 | else if (process.argv.length == 4) { 215 | args = process.argv.slice(2); 216 | let contractAddress = args[0]; 217 | let toAddress = config.collectAddress; 218 | let accounts = args[1]; 219 | collectNFT(contractAddress, toAddress, accounts); 220 | } 221 | else { 222 | console.log(`\n参数错误\n`) 223 | } 224 | } 225 | 226 | startRun() --------------------------------------------------------------------------------