├── .gitignore ├── .vscode └── settings.json ├── README.md ├── assets └── tma.jpg ├── bitcoinjs-lib ├── package-lock.json ├── package.json └── src │ ├── generate-P2PKH-wallet.ts │ ├── generate-P2WPKH-wallet.ts │ ├── import-wallet-with-mnemonic.ts │ ├── import-wallet-with-private-key.ts │ ├── sign-verify.ts │ └── transition.ts ├── blockchain ├── package-lock.json ├── package.json └── src │ ├── ch01 │ ├── Block.ts │ ├── Blockchain.ts │ └── main.ts │ └── ch02 │ ├── Block.ts │ ├── Blockchain.ts │ ├── P2PServer.ts │ ├── data │ ├── blockchain.json │ ├── blockchain1.json │ └── blockchain2.json │ └── main.ts ├── contract ├── noah-nft │ ├── build │ │ └── contracts │ │ │ ├── IERC165.json │ │ │ ├── IERC721.json │ │ │ ├── IERC721Metadata.json │ │ │ ├── IERC721Receiver.json │ │ │ └── NoahNFT.json │ ├── contracts │ │ ├── .gitkeep │ │ ├── IERC165.sol │ │ ├── IERC721.sol │ │ ├── NoahNFT.sol │ │ └── NoahNFTSwap.sol │ ├── migrations │ │ ├── .gitkeep │ │ └── 1_noah_nft.js │ ├── test │ │ ├── .gitkeep │ │ ├── noah-nft-swap.js │ │ └── noah-nft.js │ └── truffle-config.js ├── noah-token │ ├── .env.example │ ├── build │ │ └── contracts │ │ │ ├── Airdrop.json │ │ │ ├── Faucet.json │ │ │ ├── IERC20.json │ │ │ └── NoahToken.json │ ├── contracts │ │ ├── .gitkeep │ │ ├── Airdrop.sol │ │ ├── AirdropFree.sol │ │ ├── Faucet.sol │ │ ├── IERC20.sol │ │ └── NoahToken.sol │ ├── migrations │ │ ├── .gitkeep │ │ ├── 1_NoahToken_migration.js │ │ ├── 2_Faucet_migration.js │ │ ├── 3_Faucet_goerli_migration.js │ │ ├── 4_Airdrop_migration.js │ │ └── 5_Airdrop_goerli_migration.js │ ├── package-lock.json │ ├── test │ │ ├── .gitkeep │ │ ├── airdrop-free.js │ │ ├── airdrop.js │ │ ├── faucet.js │ │ └── token.js │ └── truffle-config.js ├── package-lock.json ├── package.json └── utils │ └── deploy.js ├── frontend ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .vscode │ └── settings.json ├── README.md ├── abi │ ├── Airdrop.json │ ├── Faucet.json │ ├── IERC165.json │ ├── IERC20.json │ ├── IERC721.json │ ├── IERC721Metadata.json │ ├── IERC721Receiver.json │ ├── NoahNFT.json │ └── NoahToken.json ├── components │ ├── Gtag.tsx │ ├── Icon.tsx │ ├── Layout │ │ └── Layout.tsx │ └── Profile.tsx ├── errors │ ├── creator.ts │ ├── faucet.ts │ ├── index.ts │ ├── type.d.ts │ └── upload.ts ├── examples.json ├── libs │ ├── gtag.ts │ ├── image-url.ts │ ├── swr.ts │ └── wagmi.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── airdrop.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ ├── faucet │ │ │ ├── config.ts │ │ │ └── withdraw.ts │ │ ├── nft │ │ │ ├── [id].ts │ │ │ ├── creator │ │ │ │ ├── [address].ts │ │ │ │ └── request.ts │ │ │ ├── index.ts │ │ │ ├── mint.ts │ │ │ └── sync.ts │ │ └── upload │ │ │ ├── image.ts │ │ │ └── json.ts │ ├── faucet-with-backend.tsx │ ├── faucet.tsx │ ├── index.tsx │ ├── noah-nft.tsx │ ├── noah-token.tsx │ ├── sample-wallet.tsx │ ├── wallet-generator.tsx │ └── wallet-genuine-number-generator.tsx ├── postcss.config.js ├── prisma │ ├── db.ts │ ├── migrations │ │ ├── 20230107092552_init │ │ │ └── migration.sql │ │ ├── 20230112140719_nft │ │ │ └── migration.sql │ │ ├── 20230112160742_modify_token_id_to_int │ │ │ └── migration.sql │ │ ├── 20230112171001_add_minter_request │ │ │ └── migration.sql │ │ ├── 20230115053807_rename_minter_to_creator │ │ │ └── migration.sql │ │ ├── 20230115061320_nft_creator │ │ │ └── migration.sql │ │ ├── 20230115063524_add_nft_creator │ │ │ └── migration.sql │ │ ├── 20230115170317_noah_nft_scan │ │ │ └── migration.sql │ │ ├── 20230115182057_nft_field │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── public │ ├── bg.jpg │ ├── favicon.ico │ ├── next.svg │ ├── thirteen.svg │ └── vercel.svg ├── scripts │ ├── admins.ts │ └── initialAdmin.ts ├── styles │ ├── globals.css │ └── simplebar.min.css ├── tailwind.config.js └── tsconfig.json ├── mock ├── README.md ├── airdrop │ ├── oneToMany.txt │ ├── oneToMany.xlsx │ ├── oneToOne.txt │ └── oneToOne.xlsx └── nft │ └── azuki │ ├── 1221.png │ ├── 136.png │ ├── 3782.png │ ├── 6451.png │ ├── 7231.png │ └── 7462.png ├── package-lock.json ├── package.json ├── script └── particle-network │ ├── README.md │ ├── images │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ └── 9.jpg │ ├── particle-script │ └── particle-script-Setup-0.0.6.exe └── ton └── wallet ├── balance.ts ├── create.ts ├── import.ts └── transfer.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | **/.next/ 13 | **/out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "chakra", 4 | "Formik", 5 | "gtag", 6 | "headlessui", 7 | "metadatas", 8 | "nextauth", 9 | "nomicfoundation", 10 | "QSTASH", 11 | "radash", 12 | "rainbowkit", 13 | "simplebar", 14 | "supabase", 15 | "vivus", 16 | "wagmi" 17 | ] 18 | } -------------------------------------------------------------------------------- /assets/tma.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhenqian/web3-examples/0c9023d1009de35a7101704ac2d57f67771f03ba/assets/tma.jpg -------------------------------------------------------------------------------- /bitcoinjs-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcoinjs-lib", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bip32": "^4.0.0", 14 | "bip39": "^3.1.0", 15 | "bitcoinjs-lib": "^6.1.5", 16 | "bitcoinjs-message": "^2.2.0", 17 | "ecpair": "^2.1.0", 18 | "tiny-secp256k1": "^2.2.3" 19 | }, 20 | "devDependencies": { 21 | "@types/bitcoinjs-lib": "^5.0.0", 22 | "@types/node": "^20.11.20" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bitcoinjs-lib/src/generate-P2PKH-wallet.ts: -------------------------------------------------------------------------------- 1 | import * as bitcoin from 'bitcoinjs-lib'; 2 | import ECPairFactory from 'ecpair'; 3 | import * as ecc from 'tiny-secp256k1'; 4 | 5 | // 设置比特币网络 6 | const network = bitcoin.networks.bitcoin; 7 | // 创建一个新的密钥对工厂 8 | const keyPair = ECPairFactory(ecc); 9 | // 通过密钥对工厂创建一个新的密钥对实例 10 | const keyPairInstance = keyPair.makeRandom({ network }); 11 | // 通过密钥对实例创建一个新的P2PKH地址 12 | const { address, pubkey } = bitcoin.payments.p2pkh({ pubkey: keyPairInstance.publicKey, network }); 13 | // 获取 WIF 格式的私钥(WIF 是 Wallet Import Format 的缩写,即钱包导入格式) 14 | const privateKey = keyPairInstance.toWIF(); 15 | 16 | console.debug('Address:', address); 17 | console.debug('Public key:', pubkey!.toString('hex')); 18 | console.debug('Private key:', privateKey); 19 | -------------------------------------------------------------------------------- /bitcoinjs-lib/src/generate-P2WPKH-wallet.ts: -------------------------------------------------------------------------------- 1 | import * as bitcoin from 'bitcoinjs-lib'; 2 | import ECPairFactory from 'ecpair'; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import * as bip32 from 'bip32'; 5 | import * as bip39 from 'bip39'; 6 | 7 | // 设置比特币网络 8 | const network = bitcoin.networks.bitcoin; 9 | // 生成随机的助记词 10 | const mnemonic = bip39.generateMnemonic(); 11 | // 通过助记词生成种子 12 | const seed = bip39.mnemonicToSeedSync(mnemonic); 13 | // 通过种子生成根密钥 14 | const root = bip32.BIP32Factory(ecc).fromSeed(seed, network); 15 | 16 | // m/44'/0'/0'/0/0 是 P2WPKH 的 BIP32 派生路径 17 | const path = "m/44'/0'/0'/0/0"; 18 | // 根据派生路径生成子密钥 19 | const child = root.derivePath(path); 20 | // 根据子密钥生成密钥对实例 21 | const keyPairInstance = ECPairFactory(ecc).fromPrivateKey(child.privateKey!, { network }); 22 | // 通过密钥对实例创建一个新的P2WPKH地址 23 | const { address, pubkey } = bitcoin.payments.p2wpkh({ pubkey: keyPairInstance.publicKey, network }); 24 | // 获取 WIF 格式的私钥 25 | const privateKey = keyPairInstance.toWIF(); 26 | 27 | console.debug('Address:', address); 28 | console.debug('Public key:', pubkey!.toString('hex')); 29 | console.debug('Private key:', privateKey); 30 | console.debug('Mnemonic:', mnemonic); 31 | -------------------------------------------------------------------------------- /bitcoinjs-lib/src/import-wallet-with-mnemonic.ts: -------------------------------------------------------------------------------- 1 | import BIP32Factory from 'bip32'; 2 | import { networks, payments } from 'bitcoinjs-lib'; 3 | import ECPairFactory from 'ecpair'; 4 | import * as ecc from 'tiny-secp256k1'; 5 | import * as bip39 from 'bip39'; 6 | 7 | // 助记词 8 | const mnemonic = 'YOUR_MNEMONIC_KEY_HERE'; 9 | // 通过助记词生成种子 10 | const seed = bip39.mnemonicToSeedSync(mnemonic); 11 | // 通过种子生成根密钥 12 | const root = BIP32Factory(ecc).fromSeed(seed, networks.bitcoin); 13 | // 派生路径 14 | const path = "m/44'/0'/0'/0/0"; 15 | // 通过派生路径生成子密钥 16 | const child = root.derivePath(path); 17 | // 通过子密钥生成密钥对实例 18 | const keyPairInstance = ECPairFactory(ecc).fromPrivateKey(child.privateKey!, { network: networks.bitcoin }); 19 | // 通过密钥对实例创建一个新的P2PKH地址 20 | const { address, pubkey } = payments.p2pkh({ pubkey: keyPairInstance.publicKey, network: networks.bitcoin }); 21 | // P2WPKH 地址 22 | const { address: p2wpkhAddress } = payments.p2wpkh({ pubkey: keyPairInstance.publicKey, network: networks.bitcoin }); 23 | 24 | console.debug('Address:', address); 25 | console.debug('P2WPKH Address:', p2wpkhAddress); 26 | console.debug('Public key:', pubkey!.toString('hex')); 27 | console.debug('Private key:', keyPairInstance.toWIF()); 28 | -------------------------------------------------------------------------------- /bitcoinjs-lib/src/import-wallet-with-private-key.ts: -------------------------------------------------------------------------------- 1 | import { networks, payments } from 'bitcoinjs-lib'; 2 | import ECPairFactory from 'ecpair'; 3 | import * as ecc from 'tiny-secp256k1'; 4 | 5 | // 通过 WIF 格式的私钥可以导入钱包,从而控制这个地址上的比特币。 6 | 7 | // WIF 格式的私钥 8 | const privateKey = 'YOUR_PRIVATE_KEY_HERE' 9 | // 通过 WIF 格式的私钥导入钱包 10 | const keyPair = ECPairFactory(ecc).fromWIF(privateKey); 11 | // 公钥 12 | const pubkey = keyPair.publicKey; 13 | // 比特币地址 14 | const { address } = payments.p2pkh({ pubkey, network: networks.bitcoin }); 15 | // P2WPKH 地址 16 | const { address: p2wpkhAddress } = payments.p2wpkh({ pubkey, network: networks.bitcoin }); 17 | 18 | console.debug('Address:', address); 19 | console.debug('P2WPKH Address:', p2wpkhAddress); 20 | console.debug('Public key:', pubkey.toString('hex')); 21 | console.debug('Private key:', privateKey); 22 | -------------------------------------------------------------------------------- /bitcoinjs-lib/src/sign-verify.ts: -------------------------------------------------------------------------------- 1 | import * as bitcoin from 'bitcoinjs-lib'; 2 | import { sign, verify } from 'bitcoinjs-message'; 3 | import ECPairFactory from 'ecpair'; 4 | import * as ecc from 'tiny-secp256k1'; 5 | 6 | // WIF 格式的私钥 7 | const privateKey = 'YOUR_PRIVATE_KEY_HERE' 8 | // 通过 WIF 格式的私钥导入钱包 9 | const keyPair = ECPairFactory(ecc).fromWIF(privateKey); 10 | // 签名消息 11 | const message = 'Hello, World!'; 12 | const signature = sign(message, keyPair.privateKey!, keyPair.compressed); 13 | console.debug('Signature:', signature.toString('base64')); 14 | // 获取地址 15 | const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network: bitcoin.networks.bitcoin }); 16 | // 验证签名 17 | const verified = verify(message, address!, signature); 18 | console.debug('Verified: ', verified); 19 | -------------------------------------------------------------------------------- /bitcoinjs-lib/src/transition.ts: -------------------------------------------------------------------------------- 1 | import * as bitcoin from 'bitcoinjs-lib'; 2 | import ECPairFactory from 'ecpair'; 3 | import * as ecc from 'tiny-secp256k1'; 4 | import * as bip32 from 'bip32'; 5 | import * as bip39 from 'bip39'; 6 | 7 | function generateTestnetAddress() { 8 | // 设置比特币网络 9 | const network = bitcoin.networks.testnet; 10 | // 生成随机的助记词 11 | const mnemonic = bip39.generateMnemonic(); 12 | // 通过助记词生成种子 13 | const seed = bip39.mnemonicToSeedSync(mnemonic); 14 | // 通过种子生成根密钥 15 | const root = bip32.BIP32Factory(ecc).fromSeed(seed, network); 16 | 17 | // m/44'/0'/0'/0/0 是 P2WPKH 的 BIP32 派生路径 18 | const path = "m/44'/0'/0'/0/0"; 19 | // 根据派生路径生成子密钥 20 | const child = root.derivePath(path); 21 | // 根据子密钥生成密钥对实例 22 | const keyPairInstance = ECPairFactory(ecc).fromPrivateKey(child.privateKey!, { network }); 23 | // 通过密钥对实例创建一个新的P2WPKH地址 24 | const { address, pubkey } = bitcoin.payments.p2wpkh({ pubkey: keyPairInstance.publicKey, network }); 25 | // 获取 WIF 格式的私钥 26 | const privateKey = keyPairInstance.toWIF(); 27 | 28 | console.debug('Address:', address); 29 | console.debug('Public key:', pubkey!.toString('hex')); 30 | console.debug('Private key:', privateKey); 31 | console.debug('Mnemonic:', mnemonic); 32 | 33 | return { 34 | address, 35 | pubkey: pubkey!.toString('hex'), 36 | privateKey, 37 | mnemonic 38 | } 39 | } 40 | 41 | // 通过比特币地址查询余额 42 | async function getBalance(address: string) { 43 | const url = `https://api.blockcypher.com/v1/btc/test3/addrs/${address}/balance`; 44 | const res = await fetch(url) 45 | return await res.json(); 46 | } 47 | 48 | // 查询 UTXO 49 | async function getUTXO(address: string) { 50 | const url = `https://api.blockcypher.com/v1/btc/test3/addrs/${address}?unspentOnly=true`; 51 | const res = await fetch(url) 52 | return await res.json(); 53 | } 54 | 55 | // 查询交易详情 56 | async function getTxDetail(txHash: string) { 57 | const url = `https://api.blockcypher.com/v1/btc/test3/txs/${txHash}`; 58 | const res = await fetch(url) 59 | return await res.json(); 60 | } 61 | 62 | // 广播交易 63 | async function broadcastTx(tx: string) { 64 | const res = await fetch( 65 | `https://api.blockcypher.com/v1/btc/test3/txs/push`, 66 | { 67 | method: 'POST', 68 | body: JSON.stringify({ 69 | tx, 70 | }), 71 | headers: { 72 | 'Content-Type': 'application/json' 73 | } 74 | } 75 | ); 76 | return await res.json(); 77 | } 78 | 79 | // 转账 80 | async function transfer(privateKey: string, toAddress: string, amount: number) { 81 | try { 82 | // 定义验证函数,用于校验签名是否有效 83 | const validator = ( 84 | pubkey: Buffer, 85 | msghash: Buffer, 86 | signature: Buffer, 87 | ): boolean => ECPair.fromPublicKey(pubkey).verify(msghash, signature); 88 | 89 | // 创建一个新的密钥对工厂 90 | const ECPair = ECPairFactory(ecc); 91 | // 设置发送方私钥 92 | const alice = ECPair.fromWIF(privateKey, bitcoin.networks.testnet); 93 | // 发送方地址 94 | const aliacAddress = bitcoin.payments.p2wpkh({ pubkey: alice.publicKey, network: bitcoin.networks.testnet }).address; 95 | // 动态查询 UTXO 96 | const utxo = await getUTXO(aliacAddress!); 97 | // 如果没有 UTXO,则无法进行转账,返回错误信息 98 | if (utxo.txrefs === null) { 99 | return 'No UTXO'; 100 | } 101 | // 选择最后一个 UTXO 作为输入 102 | const utxoTarget = utxo.txrefs[utxo.txrefs.length - 1]; 103 | // UTXO 的交易哈希 104 | const utxoHash = utxoTarget.tx_hash; 105 | // 查询 UTXO 对应的交易详情 106 | const txDetail = await getTxDetail(utxoHash); 107 | // 获取输出脚本的十六进制表示 108 | const scriptPubKeyHex = txDetail.outputs[0].script; 109 | // 创建一个新的 Psbt 实例 (Partially Signed Bitcoin Transaction) 110 | // 一个部分签名的比特币交易,被创建出来但还没有被完全签名的交易 111 | const psbt = new bitcoin.Psbt({ 112 | network: bitcoin.networks.testnet, 113 | }); 114 | // 设置 gas 115 | const fee = 1000; 116 | // 添加输入 117 | psbt.addInput({ 118 | // UTXO 的交易哈希 119 | hash: utxoHash, 120 | // UTXO 的输出索引 121 | index: utxoTarget.tx_output_n, 122 | witnessUtxo: { 123 | // UTXO 的输出脚本 124 | script: Buffer.from(scriptPubKeyHex, 'hex'), 125 | // UTXO 的金额 126 | value: utxoTarget.value, 127 | } 128 | }); 129 | // 添加输出 130 | psbt.addOutput({ 131 | // 接收方地址 132 | address: toAddress, 133 | // 金额 134 | value: amount, 135 | }); 136 | // 计算找零 137 | const change = utxoTarget.value - amount - fee; 138 | // 添加找零 139 | psbt.addOutput({ 140 | // 找零地址 141 | address: aliacAddress!, 142 | // 金额 143 | value: change, 144 | }); 145 | // 签名输入 146 | psbt.signInput(0, alice); 147 | // 验证输入签名 148 | psbt.validateSignaturesOfInput(0, validator); 149 | // 终结所有输入,表示签名完成 150 | psbt.finalizeAllInputs(); 151 | // 提取交易事务 152 | const tx = psbt.extractTransaction().toHex(); 153 | // 广播交易到比特币网络,等待确认 154 | const res = await broadcastTx(tx); 155 | return res; 156 | } 157 | catch (e) { 158 | console.error('transfer error: ', e); 159 | } 160 | } 161 | 162 | // 转账 163 | transfer( 164 | process.env.ALICE_PRIVATE_KEY!, 165 | process.env.BOB_ADDRESS!, 166 | 10000 167 | ).then(console.debug); 168 | 169 | // 查询余额 170 | getBalance( 171 | process.env.ALICE_ADDRESS! 172 | ).then(console.debug); 173 | 174 | // 查询 UTXO 175 | getUTXO( 176 | process.env.ALICE_ADDRESS! 177 | ).then(console.debug); 178 | -------------------------------------------------------------------------------- /blockchain/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blockchain", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "blockchain", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "crypto-js": "^4.2.0", 13 | "yargs": "^17.7.2" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^20.11.19" 17 | } 18 | }, 19 | "node_modules/@types/node": { 20 | "version": "20.11.19", 21 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", 22 | "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", 23 | "dev": true, 24 | "dependencies": { 25 | "undici-types": "~5.26.4" 26 | } 27 | }, 28 | "node_modules/ansi-regex": { 29 | "version": "5.0.1", 30 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 31 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 32 | "engines": { 33 | "node": ">=8" 34 | } 35 | }, 36 | "node_modules/ansi-styles": { 37 | "version": "4.3.0", 38 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 39 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 40 | "dependencies": { 41 | "color-convert": "^2.0.1" 42 | }, 43 | "engines": { 44 | "node": ">=8" 45 | }, 46 | "funding": { 47 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 48 | } 49 | }, 50 | "node_modules/cliui": { 51 | "version": "8.0.1", 52 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 53 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 54 | "dependencies": { 55 | "string-width": "^4.2.0", 56 | "strip-ansi": "^6.0.1", 57 | "wrap-ansi": "^7.0.0" 58 | }, 59 | "engines": { 60 | "node": ">=12" 61 | } 62 | }, 63 | "node_modules/color-convert": { 64 | "version": "2.0.1", 65 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 66 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 67 | "dependencies": { 68 | "color-name": "~1.1.4" 69 | }, 70 | "engines": { 71 | "node": ">=7.0.0" 72 | } 73 | }, 74 | "node_modules/color-name": { 75 | "version": "1.1.4", 76 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 77 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 78 | }, 79 | "node_modules/crypto-js": { 80 | "version": "4.2.0", 81 | "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", 82 | "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" 83 | }, 84 | "node_modules/emoji-regex": { 85 | "version": "8.0.0", 86 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 87 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 88 | }, 89 | "node_modules/escalade": { 90 | "version": "3.1.2", 91 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", 92 | "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", 93 | "engines": { 94 | "node": ">=6" 95 | } 96 | }, 97 | "node_modules/get-caller-file": { 98 | "version": "2.0.5", 99 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 100 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 101 | "engines": { 102 | "node": "6.* || 8.* || >= 10.*" 103 | } 104 | }, 105 | "node_modules/is-fullwidth-code-point": { 106 | "version": "3.0.0", 107 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 108 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 109 | "engines": { 110 | "node": ">=8" 111 | } 112 | }, 113 | "node_modules/require-directory": { 114 | "version": "2.1.1", 115 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 116 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 117 | "engines": { 118 | "node": ">=0.10.0" 119 | } 120 | }, 121 | "node_modules/string-width": { 122 | "version": "4.2.3", 123 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 124 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 125 | "dependencies": { 126 | "emoji-regex": "^8.0.0", 127 | "is-fullwidth-code-point": "^3.0.0", 128 | "strip-ansi": "^6.0.1" 129 | }, 130 | "engines": { 131 | "node": ">=8" 132 | } 133 | }, 134 | "node_modules/strip-ansi": { 135 | "version": "6.0.1", 136 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 137 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 138 | "dependencies": { 139 | "ansi-regex": "^5.0.1" 140 | }, 141 | "engines": { 142 | "node": ">=8" 143 | } 144 | }, 145 | "node_modules/undici-types": { 146 | "version": "5.26.5", 147 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 148 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 149 | "dev": true 150 | }, 151 | "node_modules/wrap-ansi": { 152 | "version": "7.0.0", 153 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 154 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 155 | "dependencies": { 156 | "ansi-styles": "^4.0.0", 157 | "string-width": "^4.1.0", 158 | "strip-ansi": "^6.0.0" 159 | }, 160 | "engines": { 161 | "node": ">=10" 162 | }, 163 | "funding": { 164 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 165 | } 166 | }, 167 | "node_modules/y18n": { 168 | "version": "5.0.8", 169 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 170 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 171 | "engines": { 172 | "node": ">=10" 173 | } 174 | }, 175 | "node_modules/yargs": { 176 | "version": "17.7.2", 177 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 178 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 179 | "dependencies": { 180 | "cliui": "^8.0.1", 181 | "escalade": "^3.1.1", 182 | "get-caller-file": "^2.0.5", 183 | "require-directory": "^2.1.1", 184 | "string-width": "^4.2.3", 185 | "y18n": "^5.0.5", 186 | "yargs-parser": "^21.1.1" 187 | }, 188 | "engines": { 189 | "node": ">=12" 190 | } 191 | }, 192 | "node_modules/yargs-parser": { 193 | "version": "21.1.1", 194 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 195 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 196 | "engines": { 197 | "node": ">=12" 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /blockchain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blockchain", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "LuZhenqian", 11 | "license": "ISC", 12 | "dependencies": { 13 | "crypto-js": "^4.2.0", 14 | "yargs": "^17.7.2" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^20.11.19" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /blockchain/src/ch01/Block.ts: -------------------------------------------------------------------------------- 1 | import sha256 from "crypto-js/sha256"; 2 | 3 | export class Block { 4 | index: number; // 区块索引 5 | timestamp: string; // 时间戳 6 | data: string; // 区块数据 7 | previousHash: string; // 前一个区块的哈希值 8 | hash: string; // 当前区块的哈希值 9 | nonce: number; // 随机数 10 | 11 | constructor( 12 | index: number, 13 | timestamp: string, 14 | data: string, 15 | previousHash: string = "" 16 | ) { 17 | this.index = index; 18 | this.timestamp = timestamp; 19 | this.data = data; 20 | this.previousHash = previousHash; 21 | this.hash = this.calculateHash(); 22 | this.nonce = 0; 23 | } 24 | 25 | // 计算区块哈希值 26 | calculateHash(): string { 27 | return sha256( 28 | this.index + this.previousHash + this.timestamp + this.data + this.nonce 29 | ).toString(); 30 | } 31 | 32 | // 挖掘新区块 33 | mineBlock(difficulty: number): void { 34 | while ( 35 | this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0") 36 | ) { 37 | this.nonce++; 38 | this.hash = this.calculateHash(); 39 | } 40 | console.log("Block mined: " + this.hash); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /blockchain/src/ch01/Blockchain.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "./Block"; 2 | 3 | export class Blockchain { 4 | chain: Block[]; // 区块链 5 | difficulty: number; // 工作量证明难度 6 | height: number; // 区块链高度 7 | 8 | constructor() { 9 | this.chain = [this.createGenesisBlock()]; 10 | this.difficulty = 2; 11 | this.height = 1; 12 | } 13 | 14 | // 创建创世区块 15 | createGenesisBlock(): Block { 16 | return new Block(0, '2024-02-18', "Genesis block", "0"); 17 | } 18 | 19 | // 获取最新区块 20 | getLatestBlock(): Block { 21 | return this.chain[this.chain.length - 1]; 22 | } 23 | 24 | // 添加新区块 25 | addBlock(newBlock: Block): void { 26 | newBlock.previousHash = this.getLatestBlock().hash; 27 | newBlock.mineBlock(this.difficulty); 28 | this.chain.push(newBlock); 29 | this.height++; 30 | } 31 | 32 | // 验证区块链是否有效 33 | isChainValid(): boolean { 34 | for (let i = 1; i < this.chain.length; i++) { 35 | const currentBlock = this.chain[i]; 36 | const previousBlock = this.chain[i - 1]; 37 | 38 | if (currentBlock.hash !== currentBlock.calculateHash()) { 39 | return false; 40 | } 41 | 42 | if (currentBlock.previousHash !== previousBlock.hash) { 43 | return false; 44 | } 45 | } 46 | return true; 47 | } 48 | } -------------------------------------------------------------------------------- /blockchain/src/ch01/main.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "./Block"; 2 | import { Blockchain } from "./Blockchain"; 3 | 4 | const myBlockchain = new Blockchain(); 5 | console.debug("Mining block 1..."); 6 | myBlockchain.addBlock( 7 | new Block( 8 | 1, 9 | "2024-02-18", 10 | JSON.stringify({ 11 | amount: 4, 12 | }) 13 | ) 14 | ); 15 | 16 | console.debug("Mining block 2..."); 17 | myBlockchain.addBlock( 18 | new Block( 19 | 2, 20 | "2024-02-18", 21 | JSON.stringify({ 22 | amount: 10, 23 | }) 24 | ) 25 | ); 26 | 27 | console.debug("Blockchain is valid: ", myBlockchain.isChainValid()); 28 | 29 | myBlockchain.chain[1].data = JSON.stringify({ 30 | amount: 100, 31 | }); 32 | 33 | console.debug("Blockchain is valid: ", myBlockchain.isChainValid()); -------------------------------------------------------------------------------- /blockchain/src/ch02/Block.ts: -------------------------------------------------------------------------------- 1 | import sha256 from "crypto-js/sha256"; 2 | 3 | export class Block { 4 | index: number; // 区块索引 5 | timestamp: string; // 时间戳 6 | data: string; // 区块数据 7 | previousHash: string; // 前一个区块的哈希值 8 | hash: string; // 当前区块的哈希值 9 | nonce: number; // 随机数 10 | 11 | constructor( 12 | index: number, 13 | timestamp: string, 14 | data: string, 15 | previousHash: string = "" 16 | ) { 17 | this.index = index; 18 | this.timestamp = timestamp; 19 | this.data = data; 20 | this.previousHash = previousHash; 21 | this.hash = this.calculateHash(); 22 | this.nonce = 0; 23 | } 24 | 25 | // 计算区块哈希值 26 | calculateHash(): string { 27 | return sha256( 28 | this.index + this.previousHash + this.timestamp + this.data + this.nonce 29 | ).toString(); 30 | } 31 | 32 | // 挖掘新区块 33 | mineBlock(difficulty: number): void { 34 | while ( 35 | this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0") 36 | ) { 37 | this.nonce++; 38 | this.hash = this.calculateHash(); 39 | } 40 | console.log("Block mined: " + this.hash); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /blockchain/src/ch02/Blockchain.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "./Block"; 2 | import fs from 'fs'; 3 | 4 | export class Blockchain { 5 | chain: Block[]; // 区块链 6 | difficulty: number; // 工作量证明难度 7 | dataPath: string; // 区块链数据文件路径 8 | 9 | // 区块链高度,直接使用chain.length 10 | get height(): number { 11 | return this.chain.length; 12 | } 13 | 14 | constructor( 15 | dataPath: string = 'blockchain.json', 16 | ) { 17 | this.chain = [this.createGenesisBlock()]; 18 | this.difficulty = 2; 19 | this.dataPath = dataPath; 20 | 21 | this.loadChainFromFile(); 22 | 23 | // 系统崩溃时,保存区块链到文件 24 | process.on('exit', () => this.saveChainToFile()); 25 | } 26 | 27 | // 创建创世区块 28 | createGenesisBlock(): Block { 29 | return new Block(0, '2024-02-18', "Genesis block", "0"); 30 | } 31 | 32 | // 获取最新区块 33 | getLatestBlock(): Block { 34 | return this.chain[this.chain.length - 1]; 35 | } 36 | 37 | // 添加新区块 38 | addBlock(newBlock: Block): void { 39 | newBlock.previousHash = this.getLatestBlock().hash; 40 | newBlock.mineBlock(this.difficulty); 41 | this.chain.push(newBlock); 42 | this.saveChainToFile(); 43 | } 44 | 45 | // 验证区块链是否有效 46 | isChainValid(chain?: Block[]): boolean { 47 | const targetChain = chain || this.chain; 48 | for (let i = 1; i < targetChain.length; i++) { 49 | const currentBlock = targetChain[i]; 50 | const previousBlock = targetChain[i - 1]; 51 | 52 | if (currentBlock.hash !== currentBlock.calculateHash()) { 53 | return false; 54 | } 55 | 56 | if (currentBlock.previousHash !== previousBlock.hash) { 57 | return false; 58 | } 59 | } 60 | return true; 61 | } 62 | 63 | // 保存区块链到文件 64 | saveChainToFile(): void { 65 | try { 66 | // 保存之前校验链是否被篡改 67 | if (this.isChainValid() === false) { 68 | console.error('Blockchain is not valid, not saving to file'); 69 | return; 70 | } 71 | const jsonContent = JSON.stringify(this.chain, null, 2); 72 | fs.writeFileSync(this.dataPath, jsonContent, 'utf8'); 73 | } catch (error) { 74 | console.error('Error saving the blockchain to a file', error); 75 | } 76 | } 77 | 78 | // 从文件加载区块链 79 | loadChainFromFile(): void { 80 | try { 81 | if (fs.existsSync(this.dataPath)) { 82 | const fileContent = fs.readFileSync(this.dataPath, 'utf8'); 83 | const loadedChain = JSON.parse(fileContent); 84 | this.chain = loadedChain.map((blockData: any) => { 85 | const block = new Block(blockData.index, blockData.timestamp, blockData.data, blockData.previousHash); 86 | block.nonce = blockData.nonce; 87 | block.hash = block.calculateHash(); 88 | return block; 89 | }); 90 | 91 | // 加载之后校验链是否被篡改 92 | if (this.isChainValid() === false) { 93 | console.error('Blockchain is not valid after loading from file'); 94 | process.exit(1); 95 | } 96 | } 97 | } catch (error) { 98 | console.error('Error loading the blockchain from a file', error); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /blockchain/src/ch02/data/blockchain.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": 0, 4 | "timestamp": "2024-02-18", 5 | "data": "Genesis block", 6 | "previousHash": "0", 7 | "hash": "fad31848e83409cd68237232e83e8020534610ef98a4ae587dd66093d1a506f8", 8 | "nonce": 0 9 | }, 10 | { 11 | "index": 1, 12 | "timestamp": "2024-02-18", 13 | "data": "{\"amount\":4}", 14 | "previousHash": "fad31848e83409cd68237232e83e8020534610ef98a4ae587dd66093d1a506f8", 15 | "hash": "00be5c64b833c5fde8c380667b4d3068106370ad24c288b0bfa97ce6f1c0bf03", 16 | "nonce": 162 17 | }, 18 | { 19 | "index": 2, 20 | "timestamp": "2024-02-18", 21 | "data": "{\"amount\":10}", 22 | "previousHash": "00be5c64b833c5fde8c380667b4d3068106370ad24c288b0bfa97ce6f1c0bf03", 23 | "hash": "00434897f7fd492d348b7e7374cfd63bdde69af11c40bd1bffe2d5afea6be695", 24 | "nonce": 87 25 | } 26 | ] -------------------------------------------------------------------------------- /blockchain/src/ch02/data/blockchain1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": 0, 4 | "timestamp": "2024-02-18", 5 | "data": "Genesis block", 6 | "previousHash": "0", 7 | "hash": "fad31848e83409cd68237232e83e8020534610ef98a4ae587dd66093d1a506f8", 8 | "nonce": 0 9 | }, 10 | { 11 | "index": 1, 12 | "timestamp": "2024-02-18", 13 | "data": "{\"amount\":4}", 14 | "previousHash": "fad31848e83409cd68237232e83e8020534610ef98a4ae587dd66093d1a506f8", 15 | "hash": "00be5c64b833c5fde8c380667b4d3068106370ad24c288b0bfa97ce6f1c0bf03", 16 | "nonce": 162 17 | }, 18 | { 19 | "index": 2, 20 | "timestamp": "2024-02-18", 21 | "data": "{\"amount\":10}", 22 | "previousHash": "00be5c64b833c5fde8c380667b4d3068106370ad24c288b0bfa97ce6f1c0bf03", 23 | "hash": "00434897f7fd492d348b7e7374cfd63bdde69af11c40bd1bffe2d5afea6be695", 24 | "nonce": 87 25 | } 26 | ] -------------------------------------------------------------------------------- /blockchain/src/ch02/data/blockchain2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "index": 0, 4 | "timestamp": "2024-02-18", 5 | "data": "Genesis block", 6 | "previousHash": "0", 7 | "hash": "fad31848e83409cd68237232e83e8020534610ef98a4ae587dd66093d1a506f8", 8 | "nonce": 0 9 | }, 10 | { 11 | "index": 1, 12 | "timestamp": "2024-02-18", 13 | "data": "{\"amount\":4}", 14 | "previousHash": "fad31848e83409cd68237232e83e8020534610ef98a4ae587dd66093d1a506f8", 15 | "hash": "00be5c64b833c5fde8c380667b4d3068106370ad24c288b0bfa97ce6f1c0bf03", 16 | "nonce": 162 17 | }, 18 | { 19 | "index": 2, 20 | "timestamp": "2024-02-18", 21 | "data": "{\"amount\":10}", 22 | "previousHash": "00be5c64b833c5fde8c380667b4d3068106370ad24c288b0bfa97ce6f1c0bf03", 23 | "hash": "00434897f7fd492d348b7e7374cfd63bdde69af11c40bd1bffe2d5afea6be695", 24 | "nonce": 87 25 | } 26 | ] -------------------------------------------------------------------------------- /blockchain/src/ch02/main.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "./Block"; 2 | import { Blockchain } from "./Blockchain"; 3 | import yargs from 'yargs'; 4 | import { P2PServer } from "./P2PServer"; 5 | 6 | // 解析命令行参数 7 | const argv = yargs 8 | .option('dataPath', { 9 | alias: 'd', 10 | description: 'The path to save the blockchain data file', 11 | type: 'string', 12 | }) 13 | .option('port', { 14 | alias: 'p', 15 | description: 'The port to listen for P2P connections', 16 | type: 'number', 17 | }) 18 | .option('host', { 19 | alias: 'h', 20 | description: 'The host to listen for P2P connections', 21 | type: 'string', 22 | }) 23 | .option('peers', { 24 | alias: 'ps', 25 | description: 'The seed peers to connect to', 26 | type: 'array', 27 | }) 28 | .help() 29 | .alias('help', 'h') 30 | .argv; 31 | 32 | // 如果命令行参数提供了dataPath,使用它;否则使用默认值 33 | const blockchainDataPath = argv.dataPath || 'blockchain.json'; 34 | const p2pPort = argv.port || 12315; 35 | const p2pHost = argv.host || 'localhost'; 36 | const seedPeers = argv.peers || ['localhost:12315']; 37 | 38 | const myBlockchain = new Blockchain(blockchainDataPath); 39 | 40 | const myP2PServer = new P2PServer(myBlockchain, p2pPort, p2pHost, seedPeers); 41 | 42 | myP2PServer.listen(); 43 | 44 | // 测试用例 45 | // console.debug("Mining block 1..."); 46 | // myBlockchain.addBlock( 47 | // new Block( 48 | // 1, 49 | // "2024-02-18", 50 | // JSON.stringify({ 51 | // amount: 4, 52 | // }) 53 | // ) 54 | // ); 55 | 56 | // console.debug("Mining block 2..."); 57 | // myBlockchain.addBlock( 58 | // new Block( 59 | // 2, 60 | // "2024-02-18", 61 | // JSON.stringify({ 62 | // amount: 10, 63 | // }) 64 | // ) 65 | // ); 66 | 67 | // console.debug("Blockchain is valid: ", myBlockchain.isChainValid()); 68 | 69 | // myBlockchain.chain[1].data = JSON.stringify({ 70 | // amount: 100, 71 | // }); 72 | 73 | // console.debug("Blockchain is valid: ", myBlockchain.isChainValid()); -------------------------------------------------------------------------------- /contract/noah-nft/build/contracts/IERC165.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "IERC165", 3 | "abi": [ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "bytes4", 8 | "name": "interfaceId", 9 | "type": "bytes4" 10 | } 11 | ], 12 | "name": "supportsInterface", 13 | "outputs": [ 14 | { 15 | "internalType": "bool", 16 | "name": "", 17 | "type": "bool" 18 | } 19 | ], 20 | "stateMutability": "pure", 21 | "type": "function" 22 | } 23 | ], 24 | "metadata": "{\"compiler\":{\"version\":\"0.8.17+commit.8df45f5f\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"project:/contracts/IERC165.sol\":\"IERC165\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[]},\"sources\":{\"project:/contracts/IERC165.sol\":{\"keccak256\":\"0x5101b6ad6c3a2d50f1b8060902ee4dfa55e6bbedf3c0568bc0fcc03eb4a9a316\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://9b55436690d3d9fa61ac8922cad2b1a57a1b07830a38ae8022d86003d6c30ac9\",\"dweb:/ipfs/QmPBi9z2WdgKYAiCnAThaCmBLXk2znyyAncHFjkHEMhQwp\"]}},\"version\":1}", 25 | "bytecode": "0x", 26 | "deployedBytecode": "0x", 27 | "immutableReferences": {}, 28 | "generatedSources": [], 29 | "deployedGeneratedSources": [], 30 | "sourceMap": "", 31 | "deployedSourceMap": "", 32 | "source": "// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.0;\n\ninterface IERC165 {\n function supportsInterface(bytes4 interfaceId) external pure returns (bool);\n}\n", 33 | "sourcePath": "/Users/luzhenqian/Work/web3/web3-examples/contract/noah-nft/contracts/IERC165.sol", 34 | "ast": { 35 | "absolutePath": "project:/contracts/IERC165.sol", 36 | "exportedSymbols": { 37 | "IERC165": [ 38 | 9 39 | ] 40 | }, 41 | "id": 10, 42 | "license": "MIT", 43 | "nodeType": "SourceUnit", 44 | "nodes": [ 45 | { 46 | "id": 1, 47 | "literals": [ 48 | "solidity", 49 | "^", 50 | "0.8", 51 | ".0" 52 | ], 53 | "nodeType": "PragmaDirective", 54 | "src": "33:23:0" 55 | }, 56 | { 57 | "abstract": false, 58 | "baseContracts": [], 59 | "canonicalName": "IERC165", 60 | "contractDependencies": [], 61 | "contractKind": "interface", 62 | "fullyImplemented": false, 63 | "id": 9, 64 | "linearizedBaseContracts": [ 65 | 9 66 | ], 67 | "name": "IERC165", 68 | "nameLocation": "68:7:0", 69 | "nodeType": "ContractDefinition", 70 | "nodes": [ 71 | { 72 | "functionSelector": "01ffc9a7", 73 | "id": 8, 74 | "implemented": false, 75 | "kind": "function", 76 | "modifiers": [], 77 | "name": "supportsInterface", 78 | "nameLocation": "91:17:0", 79 | "nodeType": "FunctionDefinition", 80 | "parameters": { 81 | "id": 4, 82 | "nodeType": "ParameterList", 83 | "parameters": [ 84 | { 85 | "constant": false, 86 | "id": 3, 87 | "mutability": "mutable", 88 | "name": "interfaceId", 89 | "nameLocation": "116:11:0", 90 | "nodeType": "VariableDeclaration", 91 | "scope": 8, 92 | "src": "109:18:0", 93 | "stateVariable": false, 94 | "storageLocation": "default", 95 | "typeDescriptions": { 96 | "typeIdentifier": "t_bytes4", 97 | "typeString": "bytes4" 98 | }, 99 | "typeName": { 100 | "id": 2, 101 | "name": "bytes4", 102 | "nodeType": "ElementaryTypeName", 103 | "src": "109:6:0", 104 | "typeDescriptions": { 105 | "typeIdentifier": "t_bytes4", 106 | "typeString": "bytes4" 107 | } 108 | }, 109 | "visibility": "internal" 110 | } 111 | ], 112 | "src": "108:20:0" 113 | }, 114 | "returnParameters": { 115 | "id": 7, 116 | "nodeType": "ParameterList", 117 | "parameters": [ 118 | { 119 | "constant": false, 120 | "id": 6, 121 | "mutability": "mutable", 122 | "name": "", 123 | "nameLocation": "-1:-1:-1", 124 | "nodeType": "VariableDeclaration", 125 | "scope": 8, 126 | "src": "152:4:0", 127 | "stateVariable": false, 128 | "storageLocation": "default", 129 | "typeDescriptions": { 130 | "typeIdentifier": "t_bool", 131 | "typeString": "bool" 132 | }, 133 | "typeName": { 134 | "id": 5, 135 | "name": "bool", 136 | "nodeType": "ElementaryTypeName", 137 | "src": "152:4:0", 138 | "typeDescriptions": { 139 | "typeIdentifier": "t_bool", 140 | "typeString": "bool" 141 | } 142 | }, 143 | "visibility": "internal" 144 | } 145 | ], 146 | "src": "151:6:0" 147 | }, 148 | "scope": 9, 149 | "src": "82:76:0", 150 | "stateMutability": "pure", 151 | "virtual": false, 152 | "visibility": "external" 153 | } 154 | ], 155 | "scope": 10, 156 | "src": "58:102:0", 157 | "usedErrors": [] 158 | } 159 | ], 160 | "src": "33:128:0" 161 | }, 162 | "compiler": { 163 | "name": "solc", 164 | "version": "0.8.17+commit.8df45f5f.Emscripten.clang" 165 | }, 166 | "networks": {}, 167 | "schemaVersion": "3.4.11", 168 | "updatedAt": "2023-01-10T18:46:09.556Z", 169 | "devdoc": { 170 | "kind": "dev", 171 | "methods": {}, 172 | "version": 1 173 | }, 174 | "userdoc": { 175 | "kind": "user", 176 | "methods": {}, 177 | "version": 1 178 | } 179 | } -------------------------------------------------------------------------------- /contract/noah-nft/contracts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhenqian/web3-examples/0c9023d1009de35a7101704ac2d57f67771f03ba/contract/noah-nft/contracts/.gitkeep -------------------------------------------------------------------------------- /contract/noah-nft/contracts/IERC165.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IERC165 { 6 | function supportsInterface(bytes4 interfaceId) external pure returns (bool); 7 | } 8 | -------------------------------------------------------------------------------- /contract/noah-nft/contracts/IERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IERC721 { 6 | // 事件 7 | event Transfer( 8 | address indexed from, 9 | address indexed to, 10 | uint256 indexed tokenId 11 | ); 12 | event Approval( 13 | address indexed owner, 14 | address indexed approved, 15 | uint256 indexed tokenId 16 | ); 17 | event ApprovalForAll( 18 | address indexed owner, 19 | address indexed operator, 20 | bool approved 21 | ); 22 | 23 | // 查询 24 | function balanceOf(address owner) external view returns (uint256 balance); 25 | 26 | function ownerOf(uint256 tokenId) external view returns (address owner); 27 | 28 | // 转账 29 | function safeTransferFrom( 30 | address from, 31 | address to, 32 | uint256 tokenId 33 | ) external; 34 | 35 | function safeTransferFrom( 36 | address from, 37 | address to, 38 | uint256 tokenId, 39 | bytes calldata data 40 | ) external; 41 | 42 | function transferFrom( 43 | address from, 44 | address to, 45 | uint256 tokenId 46 | ) external; 47 | 48 | // 授权 49 | function approve(address to, uint256 tokenId) external; 50 | 51 | function getApproved(uint256 tokenId) 52 | external 53 | view 54 | returns (address operator); 55 | 56 | function setApprovalForAll(address operator, bool _approved) external; 57 | 58 | function isApprovedForAll(address owner, address operator) 59 | external 60 | view 61 | returns (bool); 62 | } 63 | 64 | interface IERC721Receiver { 65 | function onERC721Received( 66 | address operator, 67 | address from, 68 | uint256 tokenId, 69 | bytes calldata data 70 | ) external returns (bytes4); 71 | } 72 | 73 | interface IERC721Metadata { 74 | function name() external view returns (string memory); 75 | 76 | function symbol() external view returns (string memory); 77 | 78 | function tokenURI(uint256 tokenId) external view returns (string memory); 79 | } 80 | -------------------------------------------------------------------------------- /contract/noah-nft/contracts/NoahNFTSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import "./IERC721.sol"; 5 | 6 | contract NoahNFTSwap is IERC721Receiver { 7 | // 挂单 8 | event Sell( 9 | address indexed seller, 10 | address indexed nftAddress, 11 | uint256 indexed tokenId, 12 | uint256 price 13 | ); 14 | // 撤单 15 | event Cancel( 16 | address indexed seller, 17 | address indexed nftAddress, 18 | uint256 indexed tokenId 19 | ); 20 | // 改价 21 | event ChangePrice( 22 | address indexed seller, 23 | address indexed nftAddress, 24 | uint256 indexed tokenId, 25 | uint256 price 26 | ); 27 | // 购买 28 | event Buy( 29 | address indexed buyer, 30 | address indexed nftAddress, 31 | uint256 indexed tokenId, 32 | uint256 price 33 | ); 34 | 35 | // 订单结构体 36 | struct Order { 37 | address seller; 38 | uint256 price; 39 | } 40 | 41 | // 订单 合约地址 => TokenId => 订单 42 | mapping(address => mapping(uint256 => Order)) public orders; 43 | // 手续费比例,万分之一 单位:wei 在交易成功时由卖家支付 44 | uint256 public feeRate; 45 | // 手续费接收者 46 | address public feeReceiver; 47 | 48 | constructor(uint256 _feeRate, address _feeReceiver) { 49 | feeRate = _feeRate; 50 | feeReceiver = _feeReceiver; 51 | } 52 | 53 | // 接收 ERC721 NFT 54 | function onERC721Received( 55 | address, 56 | address from, 57 | uint256 tokenId, 58 | bytes calldata 59 | ) external pure override returns (bytes4) { 60 | return this.onERC721Received.selector; 61 | } 62 | 63 | // 挂单 64 | function sell( 65 | address nftAddress, 66 | uint256 tokenId, 67 | uint256 price 68 | ) external { 69 | IERC721 nftContract = IERC721(nftAddress); // 声明IERC721接口合约变量 70 | // 检查订单是否存在 71 | require( 72 | orders[nftAddress][tokenId].seller == address(0), 73 | "Order already exists" 74 | ); 75 | // 检查价格是否大于 0 76 | require(price > 0, "Price should be greater than 0"); 77 | // 检查是否是合约所有者 78 | require( 79 | msg.sender == nftContract.ownerOf(tokenId), 80 | "You are not the owner" 81 | ); 82 | // 检查是否是合约接收者 83 | require( 84 | nftContract.getApproved(tokenId) == address(this), 85 | "You are not approved" 86 | ); 87 | // 检查 Token 是否存在 88 | require( 89 | nftContract.ownerOf(tokenId) != address(0), 90 | "Token does not exist" 91 | ); 92 | 93 | // 创建订单 94 | orders[nftAddress][tokenId] = Order(msg.sender, price); 95 | 96 | // 转移 NFT 97 | nftContract.safeTransferFrom(msg.sender, address(this), tokenId); 98 | 99 | // 触发事件 100 | emit Sell(msg.sender, nftAddress, tokenId, price); 101 | } 102 | 103 | // 撤单 104 | function cancel(address nftAddress, uint256 tokenId) external { 105 | // 检查订单是否存在 106 | require( 107 | orders[nftAddress][tokenId].seller != address(0), 108 | "Order does not exist" 109 | ); 110 | // 检查是否是订单所有者 111 | require( 112 | orders[nftAddress][tokenId].seller == msg.sender, 113 | "You are not the seller" 114 | ); 115 | 116 | // 删除订单 117 | delete orders[nftAddress][tokenId]; 118 | 119 | // 获取 NFT 合约 120 | IERC721 nftContract = IERC721(nftAddress); 121 | 122 | // 转移 NFT 123 | nftContract.safeTransferFrom(address(this), msg.sender, tokenId); 124 | 125 | // 触发事件 126 | emit Cancel(msg.sender, nftAddress, tokenId); 127 | } 128 | 129 | // 改价 130 | function changePrice( 131 | address nftAddress, 132 | uint256 tokenId, 133 | uint256 price 134 | ) external { 135 | // 检查订单是否存在 136 | require( 137 | orders[nftAddress][tokenId].seller != address(0), 138 | "Order does not exist" 139 | ); 140 | // 检查是否是订单所有者 141 | require( 142 | orders[nftAddress][tokenId].seller == msg.sender, 143 | "You are not the seller" 144 | ); 145 | // 检查价格是否大于 0 146 | require(price > 0, "Price should be greater than 0"); 147 | 148 | // 更新订单价格 149 | orders[nftAddress][tokenId].price = price; 150 | 151 | // 触发事件 152 | emit ChangePrice(msg.sender, nftAddress, tokenId, price); 153 | } 154 | 155 | // 购买 156 | function buy(address nftAddress, uint256 tokenId) external payable { 157 | // 检查订单是否存在 158 | require( 159 | orders[nftAddress][tokenId].seller != address(0), 160 | "Order does not exist" 161 | ); 162 | // 检查价格是否正确 163 | require( 164 | msg.value >= orders[nftAddress][tokenId].price, 165 | "Price is not correct" 166 | ); 167 | 168 | // 获取 NFT 合约 169 | IERC721 nftContract = IERC721(nftAddress); 170 | 171 | // 获取订单所有者 172 | address seller = orders[nftAddress][tokenId].seller; 173 | 174 | uint256 price = orders[nftAddress][tokenId].price; 175 | 176 | // 转移 NFT 177 | nftContract.safeTransferFrom(address(this), msg.sender, tokenId); 178 | 179 | // 计算手续费 180 | uint256 fee = (price * feeRate) / 10000; 181 | 182 | // 转移手续费 183 | payable(feeReceiver).transfer(fee); 184 | 185 | // 转移 ETH 186 | payable(seller).transfer(price - fee); 187 | 188 | // 如果有多余的 ETH,退回给买家 189 | if (msg.value > price) { 190 | payable(msg.sender).transfer(msg.value - price); 191 | } 192 | 193 | // 删除订单 194 | delete orders[nftAddress][tokenId]; 195 | 196 | // 触发事件 197 | emit Buy(msg.sender, nftAddress, tokenId, msg.value); 198 | } 199 | 200 | // 回退函数 201 | fallback() external payable {} 202 | 203 | // 接收 ETH 204 | receive() external payable { 205 | revert("You cannot send ETH to this contract"); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /contract/noah-nft/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhenqian/web3-examples/0c9023d1009de35a7101704ac2d57f67771f03ba/contract/noah-nft/migrations/.gitkeep -------------------------------------------------------------------------------- /contract/noah-nft/migrations/1_noah_nft.js: -------------------------------------------------------------------------------- 1 | const NoahNFT = artifacts.require("NoahNFT"); 2 | const { copyAbiFile, updateEnvFile } = require("../../utils/deploy"); 3 | 4 | module.exports = function (deployer) { 5 | deployer.deploy(NoahNFT, 'Noah', 'NOAH', 'https://gateway.pinata.cloud/ipfs/').then(() => { 6 | const { address } = NoahNFT; 7 | copyAbiFile('noah-nft'); 8 | updateEnvFile('NFT_CONTRACT_ADDRESS', address); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /contract/noah-nft/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhenqian/web3-examples/0c9023d1009de35a7101704ac2d57f67771f03ba/contract/noah-nft/test/.gitkeep -------------------------------------------------------------------------------- /contract/noah-nft/test/noah-nft-swap.js: -------------------------------------------------------------------------------- 1 | const NoahNFT = artifacts.require("NoahNFT"); 2 | const NoahNFTSwap = artifacts.require("NoahNFTSwap"); 3 | const BN = web3.utils.BN; 4 | 5 | contract('NoahNFTSwap', (accounts) => { 6 | const [alice, bob, carol] = accounts; 7 | const baseUrl = 'https://nft.webnext.cloud/' 8 | // 模拟 NFT 数据 9 | const [NFTNoOne, NFTNoTwo] = [{ 10 | uri: '1', 11 | }, { 12 | uri: '2', 13 | }] 14 | 15 | it('挂单', async () => { 16 | const noahNFTInstance = await NoahNFT.new('Noah', 'NOAH', baseUrl, { from: alice }); 17 | // 手续费 1%,单位万分之一 18 | const feeRate = 100; 19 | const noahNFTSwapInstance = await NoahNFTSwap.new(feeRate, alice, { from: alice }); 20 | // NFT 合约的地址 21 | const nftAddress = noahNFTInstance.address; 22 | // alice 铸造 1 个 NFT 并发送给自己 23 | await noahNFTInstance.mint(alice, NFTNoOne.uri, { from: alice }); 24 | // alice 授权 noahNFTSwapInstance 合约可以操作自己的 NFT 25 | await noahNFTInstance.approve(noahNFTSwapInstance.address, 1, { from: alice }); 26 | // 挂单价格,单位:wei 27 | const price = web3.utils.toWei('1'); 28 | // 挂单 29 | await noahNFTSwapInstance.sell(nftAddress, 1, price, { from: alice }); 30 | // 挂单成功后,NFT 所有权转移到了 noahNFTSwapInstance 合约 31 | assert.equal(await noahNFTInstance.ownerOf(1), noahNFTSwapInstance.address, "owner should be noahNFTSwapInstance"); 32 | // 查看挂单信息 33 | const order = await noahNFTSwapInstance.orders(nftAddress, 1); 34 | // 卖家应该是 alice 35 | assert.equal(order.seller, alice, "seller should be alice"); 36 | }); 37 | 38 | it('改价', async () => { 39 | const noahNFTInstance = await NoahNFT.new('Noah', 'NOAH', baseUrl, { from: alice }); 40 | // 手续费 1%,单位万分之一 41 | const feeRate = 100; 42 | const noahNFTSwapInstance = await NoahNFTSwap.new(feeRate, alice, { from: alice }); 43 | // NFT 合约的地址 44 | const nftAddress = noahNFTInstance.address; 45 | // alice 铸造 1 个 NFT 并发送给自己 46 | await noahNFTInstance.mint(alice, NFTNoOne.uri, { from: alice }); 47 | // alice 授权 noahNFTSwapInstance 合约可以操作自己的 NFT 48 | await noahNFTInstance.approve(noahNFTSwapInstance.address, 1, { from: alice }); 49 | // 挂单价格,单位:wei 50 | const price = web3.utils.toWei('1'); 51 | // 挂单 52 | await noahNFTSwapInstance.sell(nftAddress, 1, price, { from: alice }); 53 | // 改价 54 | const newPrice = web3.utils.toWei('2'); 55 | await noahNFTSwapInstance.changePrice(nftAddress, 1, newPrice, { from: alice }); 56 | // 查看挂单信息 57 | const order = await noahNFTSwapInstance.orders(nftAddress, 1); 58 | // 价格应该是 2 59 | assert.equal(order.price.toString(), newPrice.toString(), "price should be 2"); 60 | }); 61 | 62 | it('撤单', async () => { 63 | const noahNFTInstance = await NoahNFT.new('Noah', 'NOAH', baseUrl, { from: alice }); 64 | // 手续费 1%,单位万分之一 65 | const feeRate = 100; 66 | const noahNFTSwapInstance = await NoahNFTSwap.new(feeRate, alice, { from: alice }); 67 | // NFT 合约的地址 68 | const nftAddress = noahNFTInstance.address; 69 | // alice 铸造 1 个 NFT 并发送给自己 70 | await noahNFTInstance.mint(alice, NFTNoOne.uri, { from: alice }); 71 | // alice 授权 noahNFTSwapInstance 合约可以操作自己的 NFT 72 | await noahNFTInstance.approve(noahNFTSwapInstance.address, 1, { from: alice }); 73 | // 挂单价格,单位:wei 74 | const price = web3.utils.toWei('1') 75 | // 挂单 76 | await noahNFTSwapInstance.sell(nftAddress, 1, price, { from: alice }); 77 | // 取消挂单 78 | await noahNFTSwapInstance.cancel(nftAddress, 1, { from: alice }); 79 | // 查看挂单信息 80 | const order = await noahNFTSwapInstance.orders(nftAddress, 1); 81 | // 挂单价格应该是 0 82 | assert.equal(order.price.toString(), '0', "price should be 0"); 83 | // NFT 所有权应该是 alice 84 | assert.equal(await noahNFTInstance.ownerOf(1), alice, "owner should be alice"); 85 | }); 86 | 87 | it('购买', async () => { 88 | const noahNFTInstance = await NoahNFT.new('Noah', 'NOAH', baseUrl, { from: alice }); 89 | // 手续费 1%,单位万分之一 90 | const feeRate = 100; 91 | // carol 是收益账户 92 | const noahNFTSwapInstance = await NoahNFTSwap.new(feeRate, carol, { from: alice }); 93 | // NFT 合约的地址 94 | const nftAddress = noahNFTInstance.address; 95 | // alice 铸造 1 个 NFT 并发送给自己 96 | await noahNFTInstance.mint(alice, NFTNoOne.uri, { from: alice }); 97 | // alice 授权 noahNFTSwapInstance 合约可以操作自己的 NFT 98 | await noahNFTInstance.approve(noahNFTSwapInstance.address, 1, { from: alice }); 99 | // 挂单价格,单位:wei 100 | const price = web3.utils.toWei('1'); 101 | // 挂单 102 | await noahNFTSwapInstance.sell(nftAddress, 1, price, { from: alice }); 103 | // alice 原来的余额 104 | const aliceBalanceBefore = await web3.eth.getBalance(alice); 105 | // bob 原来的余额 106 | const bobBalanceBefore = await web3.eth.getBalance(bob); 107 | // carol 原来的余额 108 | const carolBalanceBefore = await web3.eth.getBalance(carol); 109 | // 计算手续费 110 | const fee = new BN(price).mul(new BN(feeRate)).div(new BN(10000)); 111 | // gas 单位 112 | const gas = 1000000; 113 | // gas 价格 114 | const gasPrice = 1000000000; 115 | // bob 购买 116 | const result = await noahNFTSwapInstance.buy(nftAddress, 1, { 117 | from: bob, value: price, 118 | gas, 119 | gasPrice 120 | }); 121 | // gasUsed 122 | const gasUsed = result.receipt.gasUsed * gasPrice; 123 | // alice 的余额应该是 aliceBalanceBefore + price - fee 124 | const aliceBalanceAfter = await web3.eth.getBalance(alice); 125 | assert.equal(aliceBalanceAfter.toString(), new BN(aliceBalanceBefore).add(new BN(price)).sub(new BN(fee)).toString(), "alice balance should be aliceBalanceBefore + price - fee"); 126 | // bob 交易后的余额 127 | const bobBalanceAfter = await web3.eth.getBalance(bob); 128 | // bob 的余额应该是 bobBalanceBefore - price - gasUsed 129 | assert.equal(bobBalanceAfter.toString(), new BN(bobBalanceBefore).sub(new BN(price)).sub(new BN(gasUsed)).toString(), "bob balance should be bobBalanceBefore - price - gasUsed"); 130 | // carol 交易后的余额 131 | const carolBalanceAfter = await web3.eth.getBalance(carol); 132 | // carol 的余额应该是 carolBalanceBefore + fee 133 | assert.equal(carolBalanceAfter.toString(), new BN(carolBalanceBefore).add(new BN(fee)).toString(), 134 | "carol balance should be carolBalanceBefore + fee"); 135 | // NFT 所有权应该是 bob 136 | assert.equal(await noahNFTInstance.ownerOf(1), bob, "owner should be bob"); 137 | // 挂单信息应该被清除 138 | const order = await noahNFTSwapInstance.orders(nftAddress, 1); 139 | assert.equal(order.price.toString(), '0', "price should be 0"); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /contract/noah-nft/test/noah-nft.js: -------------------------------------------------------------------------------- 1 | const NoahNFT = artifacts.require("NoahNFT"); 2 | 3 | contract('NoahNFT', (accounts) => { 4 | const [alice, bob, carol] = accounts; 5 | const baseUrl = 'https://nft.webnext.cloud/' 6 | // 模拟 NFT 数据 7 | const [NFTNoOne, NFTNoTwo] = [{ 8 | uri: '1', 9 | }, { 10 | uri: '2', 11 | }] 12 | 13 | it('mint', async () => { 14 | const noahInstance = await NoahNFT.new('Noah', 'NOAH', baseUrl, { from: alice }); 15 | // alice 铸造 1 个 NFT 并发送给 bob 16 | await noahInstance.mint(bob, NFTNoOne.uri, { from: alice }); 17 | // 检查 bob 的 NFT 数量是否为 1 18 | const bobNFTCount = await noahInstance.balanceOf(bob); 19 | assert.equal(bobNFTCount.valueOf(), 1, "bob 应该有 1 个 NFT"); 20 | // 检查 NFT 的所有者是否为 bob 21 | const tokenId = await noahInstance.totalSupply(); 22 | const bobNFT = await noahInstance.ownerOf(tokenId); 23 | assert.equal(bobNFT.valueOf(), bob, "bob 应该是 NFT 的所有者"); 24 | // 检查 NFT 的 URI 是否正确 25 | const bobNFTURI = await noahInstance.tokenURI(tokenId, { from: bob }); 26 | assert.equal(bobNFTURI.valueOf(), baseUrl + NFTNoOne.uri, "NFT 的 URI 不正确"); 27 | // bob 成为 NFT 的发行者,可以铸造 NFT 28 | await noahInstance.mint(bob, NFTNoTwo.uri, { from: bob }); 29 | // 检查 bob 的 NFT 数量是否为 2 30 | const bobNFTCount2 = await noahInstance.balanceOf(bob); 31 | assert.equal(bobNFTCount2.valueOf(), 2, "bob 应该有 2 个 NFT"); 32 | }); 33 | 34 | it("transferFrom", async () => { 35 | const noahInstance = await NoahNFT.new('Noah', 'NOAH', { from: alice }); 36 | // alice 铸造 1 个 NFT 并发送给 bob 37 | await noahInstance.mint(bob, NFTNoOne.uri, { from: alice }); 38 | // 检查 bob 的 NFT 数量是否为 1 39 | const bobNFTCount = await noahInstance.balanceOf(bob); 40 | assert.equal(bobNFTCount.valueOf(), 1, "bob 应该有 1 个 NFT"); 41 | // bob 转移 id 为 1 的 NFT 给 carol 42 | await noahInstance.transferFrom(bob, carol, 1, { from: bob }); 43 | // 检查 bob 的 NFT 数量是否为 0 44 | const bobNFTCount2 = await noahInstance.balanceOf(bob); 45 | assert.equal(bobNFTCount2.valueOf(), 0, "bob 应该有 0 个 NFT"); 46 | // 检查 carol 的 NFT 数量是否为 1 47 | const carolNFTCount = await noahInstance.balanceOf(carol); 48 | assert.equal(carolNFTCount.valueOf(), 1, "carol 应该有 1 个 NFT"); 49 | }); 50 | 51 | it("approval", async () => { 52 | const noahInstance = await NoahNFT.new('Noah', 'NOAH', { from: alice }); 53 | // alice 铸造 1 个 NFT 并发送给 bob 54 | await noahInstance.mint(bob, NFTNoOne.uri, { from: alice }); 55 | // 检查 bob 的 NFT 数量是否为 1 56 | const bobNFTCount = await noahInstance.balanceOf(bob); 57 | assert.equal(bobNFTCount.valueOf(), 1, "bob 应该有 1 个 NFT"); 58 | // bob 授权 carol 转移 id 为 1 的 NFT 59 | await noahInstance.approve(carol, 1, { from: bob }); 60 | // 检查 id 为 1 的 NFT 是否被 carol 授权 61 | const carolApproved = await noahInstance.getApproved(1, { from: carol }); 62 | assert.equal(carolApproved.valueOf(), carol, "carol 应该被授权转移 id 为 1 的 NFT"); 63 | // carol 转移 id 为 1 的 NFT 给 alice 64 | await noahInstance.transferFrom(bob, alice, 1, { from: carol }); 65 | // 检查 bob 的 NFT 数量是否为 0 66 | const bobNFTCount2 = await noahInstance.balanceOf(bob); 67 | assert.equal(bobNFTCount2.valueOf(), 0, "bob 应该有 0 个 NFT"); 68 | // 检查 alice 的 NFT 数量是否为 1 69 | const aliceNFTCount = await noahInstance.balanceOf(alice); 70 | assert.equal(aliceNFTCount.valueOf(), 1, "alice 应该有 1 个 NFT"); 71 | }); 72 | 73 | it("setApprovalForAll", async () => { 74 | const noahInstance = await NoahNFT.new('Noah', 'NOAH', { from: alice }); 75 | // alice 铸造 2 个 NFT 并发送给 bob 76 | await noahInstance.mint(bob, NFTNoOne.uri, { from: alice }); 77 | await noahInstance.mint(bob, NFTNoTwo.uri, { from: alice }); 78 | // 检查 bob 的 NFT 数量是否为 2 79 | const bobNFTCount = await noahInstance.balanceOf(bob); 80 | assert.equal(bobNFTCount.valueOf(), 2, "bob 应该有 2 个 NFT"); 81 | // bob 授权 carol 转移所有 NFT 82 | await noahInstance.setApprovalForAll(carol, true, { from: bob }); 83 | // 检查 carol 是否被授权转移所有 NFT 84 | const carolApproved = await noahInstance.isApprovedForAll(bob, carol, { from: carol }); 85 | assert.equal(carolApproved.valueOf(), true, "carol 应该被授权转移所有 NFT"); 86 | // carol 转移 id 为 1 的 NFT 给 alice 87 | await noahInstance.transferFrom(bob, alice, 1, { from: carol }); 88 | // 检查 bob 的 NFT 数量是否为 1 89 | const bobNFTCount2 = await noahInstance.balanceOf(bob); 90 | assert.equal(bobNFTCount2.valueOf(), 1, "bob 应该有 1 个 NFT"); 91 | // 检查 alice 的 NFT 数量是否为 1 92 | const aliceNFTCount = await noahInstance.balanceOf(alice); 93 | assert.equal(aliceNFTCount.valueOf(), 1, "alice 应该有 1 个 NFT"); 94 | // 检查 id 为 2 的 NFT 所有者是否为 bob 95 | const NFTNoTwoOwner = await noahInstance.ownerOf(2, { from: bob }); 96 | assert.equal(NFTNoTwoOwner.valueOf(), bob, "id 为 2 的 NFT 所有者应该是 bob"); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /contract/noah-nft/truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation, and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * https://trufflesuite.com/docs/truffle/reference/configuration 10 | * 11 | * Hands-off deployment with Infura 12 | * -------------------------------- 13 | * 14 | * Do you have a complex application that requires lots of transactions to deploy? 15 | * Use this approach to make deployment a breeze 🏖️: 16 | * 17 | * Infura deployment needs a wallet provider (like @truffle/hdwallet-provider) 18 | * to sign transactions before they're sent to a remote public node. 19 | * Infura accounts are available for free at 🔍: https://infura.io/register 20 | * 21 | * You'll need a mnemonic - the twelve word phrase the wallet uses to generate 22 | * public/private key pairs. You can store your secrets 🤐 in a .env file. 23 | * In your project root, run `$ npm install dotenv`. 24 | * Create .env (which should be .gitignored) and declare your MNEMONIC 25 | * and Infura PROJECT_ID variables inside. 26 | * For example, your .env file will have the following structure: 27 | * 28 | * MNEMONIC = 29 | * PROJECT_ID = 30 | * 31 | * Deployment with Truffle Dashboard (Recommended for best security practice) 32 | * -------------------------------------------------------------------------- 33 | * 34 | * Are you concerned about security and minimizing rekt status 🤔? 35 | * Use this method for best security: 36 | * 37 | * Truffle Dashboard lets you review transactions in detail, and leverages 38 | * MetaMask for signing, so there's no need to copy-paste your mnemonic. 39 | * More details can be found at 🔎: 40 | * 41 | * https://trufflesuite.com/docs/truffle/getting-started/using-the-truffle-dashboard/ 42 | */ 43 | 44 | // require('dotenv').config(); 45 | // const { MNEMONIC, PROJECT_ID } = process.env; 46 | 47 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 48 | 49 | module.exports = { 50 | /** 51 | * Networks define how you connect to your ethereum client and let you set the 52 | * defaults web3 uses to send transactions. If you don't specify one truffle 53 | * will spin up a managed Ganache instance for you on port 9545 when you 54 | * run `develop` or `test`. You can ask a truffle command to use a specific 55 | * network from the command line, e.g 56 | * 57 | * $ truffle test --network 58 | */ 59 | 60 | networks: { 61 | // Useful for testing. The `development` name is special - truffle uses it by default 62 | // if it's defined here and no other network is specified at the command line. 63 | // You should run a client (like ganache, geth, or parity) in a separate terminal 64 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 65 | // options below to some value. 66 | // 67 | development: { 68 | host: "127.0.0.1", // Localhost (default: none) 69 | port: 7545, // Standard Ethereum port (default: none) 70 | network_id: "*", // Any network (default: none) 71 | }, 72 | // 73 | // An additional network, but with some advanced options… 74 | // advanced: { 75 | // port: 8777, // Custom port 76 | // network_id: 1342, // Custom network 77 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 78 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 79 | // from:
, // Account to send transactions from (default: accounts[0]) 80 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 81 | // }, 82 | // 83 | // Useful for deploying to a public network. 84 | // Note: It's important to wrap the provider as a function to ensure truffle uses a new provider every time. 85 | // goerli: { 86 | // provider: () => new HDWalletProvider(MNEMONIC, `https://goerli.infura.io/v3/${PROJECT_ID}`), 87 | // network_id: 5, // Goerli's id 88 | // confirmations: 2, // # of confirmations to wait between deployments. (default: 0) 89 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 90 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 91 | // }, 92 | // 93 | // Useful for private networks 94 | // private: { 95 | // provider: () => new HDWalletProvider(MNEMONIC, `https://network.io`), 96 | // network_id: 2111, // This network is yours, in the cloud. 97 | // production: true // Treats this network as if it was a public net. (default: false) 98 | // } 99 | }, 100 | 101 | // Set default mocha options here, use special reporters, etc. 102 | mocha: { 103 | // timeout: 100000 104 | }, 105 | 106 | // Configure your compilers 107 | compilers: { 108 | solc: { 109 | version: "0.8.17" // Fetch exact version from solc-bin (default: truffle's version) 110 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 111 | // settings: { // See the solidity docs for advice about optimization and evmVersion 112 | // optimizer: { 113 | // enabled: false, 114 | // runs: 200 115 | // }, 116 | // evmVersion: "byzantium" 117 | // } 118 | } 119 | } 120 | 121 | // Truffle DB is currently disabled by default; to enable it, change enabled: 122 | // false to enabled: true. The default storage location can also be 123 | // overridden by specifying the adapter settings, as shown in the commented code below. 124 | // 125 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 126 | // make a backup of your artifacts to a safe location before enabling this feature. 127 | // 128 | // After you backed up your artifacts you can utilize db by running migrate as follows: 129 | // $ truffle migrate --reset --compile-all 130 | // 131 | // db: { 132 | // enabled: false, 133 | // host: "127.0.0.1", 134 | // adapter: { 135 | // name: "indexeddb", 136 | // settings: { 137 | // directory: ".db" 138 | // } 139 | // } 140 | // } 141 | }; 142 | -------------------------------------------------------------------------------- /contract/noah-token/.env.example: -------------------------------------------------------------------------------- 1 | # ! 开头的是必填项 2 | 3 | # 你的钱包地址私钥,用于将合约部署到 goerli 测试网。(本地开发环境不需要配置) 4 | PRIVATE_KEY="" 5 | # 你的 infura 项目 ID,用于将合约部署到 goerli 测试网(本地开发环境不需要配置) 6 | PROJECT_ID="" 7 | # 你的 NOAH 代币合约地址,用于部署 Faucet 和 Airdrop 合约 8 | NOAH_TOKEN_CONTRACT_ADDRESS="" -------------------------------------------------------------------------------- /contract/noah-token/contracts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhenqian/web3-examples/0c9023d1009de35a7101704ac2d57f67771f03ba/contract/noah-token/contracts/.gitkeep -------------------------------------------------------------------------------- /contract/noah-token/contracts/Airdrop.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./IERC20.sol"; 6 | 7 | contract Airdrop { 8 | IERC20 public tokenContract; // 代币合约 9 | address public owner; // 合约发布者 10 | 11 | constructor(address _tokenContractAddress) { 12 | tokenContract = IERC20(_tokenContractAddress); 13 | owner = msg.sender; 14 | } 15 | 16 | // 空投代币,多个地址对应一个数量 17 | function oneToMany(address[] memory _to, uint256 _amount) public { 18 | // 只有合约发布者可以调用 19 | require(msg.sender == owner, "Only the owner can airdrop tokens"); 20 | // 验证合约中的代币数量是否足够 21 | uint256 totalAmount = _amount * _to.length; 22 | require( 23 | tokenContract.balanceOf(address(this)) >= totalAmount, 24 | "Not enough tokens in the contract" 25 | ); 26 | // 空投代币 27 | for (uint256 i = 0; i < _to.length; i++) { 28 | tokenContract.transfer(_to[i], _amount); 29 | } 30 | } 31 | 32 | // 空投代币,一个地址对应一个数量 33 | function oneToOne(address[] memory _to, uint256[] memory _amount) public { 34 | // 只有合约发布者可以调用 35 | require(msg.sender == owner, "Only the owner can airdrop tokens"); 36 | // 验证数组长度是否相等 37 | require( 38 | _to.length == _amount.length, 39 | "The length of the two arrays must be the same" 40 | ); 41 | // 验证合约中的代币是否足够 42 | uint256 totalAmount = 0; 43 | for (uint256 i = 0; i < _amount.length; i++) { 44 | totalAmount += _amount[i]; 45 | } 46 | require( 47 | tokenContract.balanceOf(address(this)) >= totalAmount, 48 | "Not enough tokens in the contract" 49 | ); 50 | // 空投代币 51 | for (uint256 i = 0; i < _to.length; i++) { 52 | tokenContract.transfer(_to[i], _amount[i]); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contract/noah-token/contracts/AirdropFree.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./IERC20.sol"; 6 | 7 | // 任何人都可以调用,但是需要支付手续费 8 | contract AirdropFree { 9 | IERC20 public tokenContract; // 代币合约 10 | address public owner; // 合约发布者 11 | address private _marketingWalletAddress; // 营销钱包地址,用于收取手续费 12 | uint256 private _feeRate; // 手续费比例,单位:万分之一 13 | 14 | constructor( 15 | address _tokenContractAddress, 16 | address _marketingWallet, 17 | uint256 _fee 18 | ) { 19 | tokenContract = IERC20(_tokenContractAddress); 20 | _marketingWalletAddress = _marketingWallet; 21 | _feeRate = _fee; 22 | owner = msg.sender; 23 | } 24 | 25 | // 空投代币,多个地址对应一个数量 26 | function oneToMany(address[] memory _to, uint256 _amount) public { 27 | uint256 totalAmount = _amount * _to.length; 28 | // 计算手续费 29 | uint256 fee = (totalAmount * _feeRate) / 10000; 30 | // 增加手续费 31 | totalAmount += fee; 32 | // 验证调用者的代币数量是否足够 33 | require( 34 | tokenContract.balanceOf(msg.sender) >= totalAmount, 35 | "Not enough tokens in the address" 36 | ); 37 | // 检查调用者授权数量是否足够 38 | require( 39 | tokenContract.allowance(msg.sender, address(this)) >= totalAmount, 40 | "Not enough tokens approved" 41 | ); 42 | // 空投代币 43 | for (uint256 i = 0; i < _to.length; i++) { 44 | tokenContract.transferFrom(msg.sender, _to[i], _amount); 45 | } 46 | // 转移手续费 47 | tokenContract.transferFrom(msg.sender, _marketingWalletAddress, fee); 48 | } 49 | 50 | // 空投代币,一个地址对应一个数量 51 | function oneToOne(address[] memory _to, uint256[] memory _amount) public { 52 | // 验证数组长度是否相等 53 | require( 54 | _to.length == _amount.length, 55 | "The length of the two arrays must be the same" 56 | ); 57 | // 计算总数量 58 | uint256 totalAmount = 0; 59 | // 计算手续费 60 | uint256 fee = 0; 61 | for (uint256 i = 0; i < _amount.length; i++) { 62 | totalAmount += _amount[i]; 63 | fee += (_amount[i] * _feeRate) / 10000; 64 | } 65 | // 增加手续费 66 | totalAmount += fee; 67 | // 验证调用者的代币数量是否足够 68 | require( 69 | tokenContract.balanceOf(msg.sender) >= totalAmount, 70 | "Not enough tokens in the address" 71 | ); 72 | // 检查调用者授权数量是否足够 73 | require( 74 | tokenContract.allowance(msg.sender, address(this)) >= totalAmount, 75 | "Not enough tokens approved" 76 | ); 77 | // 空投代币 78 | for (uint256 i = 0; i < _to.length; i++) { 79 | tokenContract.transferFrom(msg.sender, _to[i], _amount[i]); 80 | } 81 | // 转移手续费 82 | tokenContract.transferFrom(msg.sender, _marketingWalletAddress, fee); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /contract/noah-token/contracts/Faucet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./IERC20.sol"; 6 | 7 | contract Faucet { 8 | IERC20 public tokenContract; // 代币合约 9 | mapping(address => uint256) public recivedRecord; // 领取记录 10 | uint256 public amountEachTime; // 每次领取的数量 11 | address public owner; // 合约发布者 12 | 13 | constructor(address _tokenContractAddress, uint256 _amountEachTime) { 14 | tokenContract = IERC20(_tokenContractAddress); 15 | amountEachTime = _amountEachTime; 16 | owner = msg.sender; 17 | } 18 | 19 | // 领取代币,每个地址每24小时只能领取一次 20 | function withdraw() external { 21 | if (recivedRecord[msg.sender] > 0) { 22 | require( 23 | recivedRecord[msg.sender] - block.timestamp >= 1 days, 24 | "You can only request tokens once every 24 hours" 25 | ); 26 | } 27 | require( 28 | tokenContract.balanceOf(address(this)) >= amountEachTime, 29 | "Not enough tokens in the contract" 30 | ); 31 | recivedRecord[msg.sender] = block.timestamp; // 更新领取记录 32 | tokenContract.transfer(msg.sender, amountEachTime); // 转账 33 | } 34 | 35 | // 设置每次领取的数量,只有合约发布者可以调用 36 | function setAmountEachTime(uint256 _amountEachTime) public { 37 | require(msg.sender == owner, "Only the owner can set the amount"); 38 | amountEachTime = _amountEachTime; // 更新每次领取的数量 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contract/noah-token/contracts/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IERC20 { 6 | function name() external view returns (string memory); 7 | 8 | function symbol() external view returns (string memory); 9 | 10 | function decimals() external view returns (uint8); 11 | 12 | function totalSupply() external view returns (uint256); 13 | 14 | function balanceOf(address _owner) external view returns (uint256 balance); 15 | 16 | function transfer(address _to, uint256 _value) 17 | external 18 | returns (bool success); 19 | 20 | function transferFrom( 21 | address _from, 22 | address _to, 23 | uint256 _value 24 | ) external returns (bool success); 25 | 26 | function approve(address _spender, uint256 _value) 27 | external 28 | returns (bool success); 29 | 30 | function allowance(address _owner, address _spender) 31 | external 32 | view 33 | returns (uint256 remaining); 34 | 35 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 36 | 37 | event Approval( 38 | address indexed _owner, 39 | address indexed _spender, 40 | uint256 _value 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /contract/noah-token/contracts/NoahToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./IERC20.sol"; 6 | 7 | contract NoahToken is IERC20 { 8 | string private _name; // 代币名称 9 | string private _symbol; // 代币代号 10 | uint8 private _decimals; // 代币精度 11 | uint256 private _totalSupply; // 代币发行总量 12 | mapping(address => uint256) private _balances; // 账本 13 | mapping(address => mapping(address => uint256)) private _allowance; // 授权记录 14 | address public owner; // 合约发布者 15 | 16 | constructor( 17 | string memory _initName, 18 | string memory _initSymbol, 19 | uint8 _initDecimals, 20 | uint256 _initTotalSupply 21 | ) { 22 | // 发布合约时设置代币名称、代号、精度和发行总量 23 | _name = _initName; 24 | _symbol = _initSymbol; 25 | _decimals = _initDecimals; 26 | _totalSupply = _initTotalSupply; 27 | owner = msg.sender; 28 | // 在合约部署时把所有的代币发行给合约发布者 29 | _balances[owner] = _initTotalSupply; 30 | } 31 | 32 | function name() external view override returns (string memory) { 33 | return _name; 34 | } 35 | 36 | function symbol() external view override returns (string memory) { 37 | return _symbol; 38 | } 39 | 40 | function decimals() external view override returns (uint8) { 41 | return _decimals; 42 | } 43 | 44 | function totalSupply() external view override returns (uint256) { 45 | return _totalSupply; 46 | } 47 | 48 | function balanceOf(address _owner) 49 | external 50 | view 51 | override 52 | returns (uint256 balance) 53 | { 54 | return _balances[_owner]; 55 | } 56 | 57 | function transfer(address _to, uint256 _value) 58 | external 59 | override 60 | returns (bool success) 61 | { 62 | // 检查发送者余额是否足够 63 | require(_balances[msg.sender] >= _value, "Insufficient balance"); 64 | // 扣除发送者余额 65 | _balances[msg.sender] -= _value; 66 | // 增加接收者余额 67 | _balances[_to] += _value; 68 | // 触发转账事件 69 | emit Transfer(msg.sender, _to, _value); 70 | return true; 71 | } 72 | 73 | function transferFrom( 74 | address _from, 75 | address _to, 76 | uint256 _value 77 | ) external override returns (bool success) { 78 | // 检查发送者余额是否足够 79 | require(_balances[_from] >= _value, "Insufficient balance"); 80 | // 检查授权额度是否足够 81 | require( 82 | _allowance[_from][msg.sender] >= _value, 83 | "Insufficient allowance" 84 | ); 85 | // 扣除发送者余额 86 | _balances[_from] -= _value; 87 | // 增加接收者余额 88 | _balances[_to] += _value; 89 | // 扣除授权额度 90 | _allowance[_from][msg.sender] -= _value; 91 | // 触发转账事件 92 | emit Transfer(_from, _to, _value); 93 | return true; 94 | } 95 | 96 | function approve(address _spender, uint256 _value) 97 | external 98 | override 99 | returns (bool success) 100 | { 101 | // 设置授权额度 102 | _allowance[msg.sender][_spender] = _value; 103 | // 触发授权事件 104 | emit Approval(msg.sender, _spender, _value); 105 | return true; 106 | } 107 | 108 | function allowance(address _owner, address _spender) 109 | external 110 | view 111 | override 112 | returns (uint256 remaining) 113 | { 114 | return _allowance[_owner][_spender]; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /contract/noah-token/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhenqian/web3-examples/0c9023d1009de35a7101704ac2d57f67771f03ba/contract/noah-token/migrations/.gitkeep -------------------------------------------------------------------------------- /contract/noah-token/migrations/1_NoahToken_migration.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(NoahToken, 'noah', 'NOAH', 18, '1024000000000000000000'); 5 | } -------------------------------------------------------------------------------- /contract/noah-token/migrations/2_Faucet_migration.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | const Faucet = artifacts.require("Faucet"); 3 | 4 | module.exports = function (deployer) { 5 | deployer.deploy(NoahToken, 'noah', 'NOAH', 18, '1024000000000000000000').then(function () { 6 | return deployer.deploy(Faucet, NoahToken.address, 1); 7 | }); 8 | } -------------------------------------------------------------------------------- /contract/noah-token/migrations/3_Faucet_goerli_migration.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | const Faucet = artifacts.require("Faucet"); 3 | 4 | module.exports = function (deployer) { 5 | deployer.deploy(Faucet, process.env.NOAH_TOKEN_CONTRACT_ADDRESS, 1); 6 | } 7 | -------------------------------------------------------------------------------- /contract/noah-token/migrations/4_Airdrop_migration.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | const Faucet = artifacts.require("Faucet"); 3 | const Airdrop = artifacts.require("Airdrop"); 4 | const fs = require("fs-extra"); 5 | const path = require("path"); 6 | 7 | module.exports = function (deployer) { 8 | deployer.deploy(NoahToken, 'noah', 'NOAH', 18, '1024000000000000000000').then(() => { 9 | return deployer.deploy(Faucet, NoahToken.address, 1); 10 | }).then(() => { 11 | return deployer.deploy(Airdrop, NoahToken.address); 12 | }).then(() => { 13 | updateEnvFile( 14 | NoahToken.address, 15 | Faucet.address, 16 | Airdrop.address 17 | ); 18 | copyAbiFile(); 19 | }); 20 | } 21 | 22 | function copyAbiFile() { 23 | const srcPath = path.resolve(__dirname, `../build/contracts/`) 24 | const destPath = path.resolve(__dirname, `../../../frontend/abi/`) 25 | const res = fs.copySync(srcPath, destPath, { 26 | overwrite: true, 27 | }) 28 | } 29 | 30 | function updateEnvFile(tokenAddress, faucetAddress, airdropAddress) { 31 | const envPath = path.resolve(__dirname, '../../../frontend/.env.local') 32 | const envFile = fs.readFileSync(envPath, 'utf-8') 33 | let env = envFile.toString() 34 | env = env.replace(/NEXT_PUBLIC_TOKEN_CONTRACT_ADDRESS=.*\n/g, 35 | `NEXT_PUBLIC_TOKEN_CONTRACT_ADDRESS=${tokenAddress}\n`) 36 | env = env.replace(/NEXT_PUBLIC_FAUCET_CONTRACT_ADDRESS=.*\n/g, 37 | `NEXT_PUBLIC_FAUCET_CONTRACT_ADDRESS=${faucetAddress}\n`) 38 | env = env.replace(/NEXT_PUBLIC_AIRDROP_CONTRACT_ADDRESS=.*\n/g, 39 | `NEXT_PUBLIC_AIRDROP_CONTRACT_ADDRESS=${airdropAddress}\n`) 40 | fs.writeFileSync(envPath, env); 41 | } 42 | -------------------------------------------------------------------------------- /contract/noah-token/migrations/5_Airdrop_goerli_migration.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | const Airdrop = artifacts.require("Airdrop"); 3 | 4 | module.exports = function (deployer) { 5 | deployer.deploy(Airdrop, process.env.NOAH_TOKEN_CONTRACT_ADDRESS); 6 | } 7 | -------------------------------------------------------------------------------- /contract/noah-token/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luzhenqian/web3-examples/0c9023d1009de35a7101704ac2d57f67771f03ba/contract/noah-token/test/.gitkeep -------------------------------------------------------------------------------- /contract/noah-token/test/airdrop-free.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | const AirdropFree = artifacts.require("AirdropFree"); 3 | 4 | contract("AirdropFree", (accounts) => { 5 | const [alice, bob, carol, dave] = accounts; 6 | 7 | it("oneToMany", async () => { 8 | // 发 Noah 币,发行 10240000 个 9 | const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '10240000', { from: alice }); 10 | // 发空投合约 设置营销收款账户为 dave;手续费为万分之 10(0.001) 11 | const airdropInstance = await AirdropFree.new(noahTokenInstance.address, dave, 10, { from: alice }); 12 | // 授权空投合约可以操作 10000 个 Noah 币 13 | await noahTokenInstance.approve(airdropInstance.address, 10000, { from: alice }); 14 | // 给 2 个账户发空投,每个账户 1000 个 Noah 币 15 | const amount = 1000; 16 | await airdropInstance.oneToMany([bob, carol], amount, { from: alice }); 17 | // 检查 2 个账户的 Noah 币数量 18 | const bobBalance = await noahTokenInstance.balanceOf(bob); 19 | const carolBalance = await noahTokenInstance.balanceOf(carol); 20 | assert.equal(bobBalance.toString(), amount, "bob balance is not 1000"); 21 | assert.equal(carolBalance.toString(), amount, "carol balance is not 1000"); 22 | const daveBalance = await noahTokenInstance.balanceOf(dave); 23 | const fee = amount * 2 * 0.001// 手续费 24 | // 检查 dave 的营销收款是否正确 25 | assert.equal(daveBalance.toString(), fee, "dave balance is not 2"); 26 | const airdropTotalAmount = amount * 2 + fee;// 空投总费用 27 | // 检查 alice 的 Noah 币数量 28 | const aliceBalance = await noahTokenInstance.balanceOf(alice); 29 | assert.equal(aliceBalance.toString(), 10240000 - airdropTotalAmount, "alice balance is not 10237998"); 30 | }); 31 | 32 | it("oneToOne", async () => { 33 | // 发 Noah 币,发行 10240000 个 34 | const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '10240000', { from: alice }); 35 | // 发空投合约 36 | const airdropInstance = await AirdropFree.new(noahTokenInstance.address, dave, 10, { from: alice }); 37 | // 授权空投合约可以操作 10000 个 Noah 币 38 | await noahTokenInstance.approve(airdropInstance.address, 10000, { from: alice }); 39 | // 给 2 个账户发空投,bob 1000 个,carol 2000 个 40 | const amounts = [1000, 2000]; 41 | await airdropInstance.oneToOne([bob, carol], amounts, { from: alice }); 42 | // 检查 2 个账户的 Noah 币数量 43 | const bobBalance = await noahTokenInstance.balanceOf(bob); 44 | const carolBalance = await noahTokenInstance.balanceOf(carol); 45 | assert.equal(bobBalance.toString(), amounts[0], "bob balance is not 10"); 46 | assert.equal(carolBalance.toString(), amounts[1], "carol balance is not 15"); 47 | const fee = amounts[0] * 0.001 + amounts[1] * 0.001// 手续费 48 | // 检查 dave 的营销收款是否正确 49 | const daveBalance = await noahTokenInstance.balanceOf(dave); 50 | assert.equal(daveBalance.toString(), fee, "dave balance is not 3"); 51 | const airdropTotalAmount = amounts[0] + amounts[1] + fee;// 空投总费用 52 | // 检查 alice 的 Noah 币数量 53 | const aliceBalance = await noahTokenInstance.balanceOf(alice); 54 | assert.equal(aliceBalance.toString(), 10240000 - airdropTotalAmount, "alice balance is not 10236997"); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /contract/noah-token/test/airdrop.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | const Airdrop = artifacts.require("Airdrop"); 3 | 4 | contract("Airdrop", (accounts) => { 5 | const [alice, bob, carol, dave] = accounts; 6 | 7 | it("oneToMany", async () => { 8 | // 发 Noah 币,发行 1024 个 9 | const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice }); 10 | // 发空投合约 11 | const airdropInstance = await Airdrop.new(noahTokenInstance.address, { from: alice }); 12 | // 给空投合约转账 100 个 Noah 币 13 | const airdropTotalAmount = 100; 14 | await noahTokenInstance.transfer(airdropInstance.address, airdropTotalAmount, { from: alice }); 15 | // 给 3 个账户发空投,每个账户 10 个 Noah 币 16 | const amount = 10; 17 | await airdropInstance.oneToMany([bob, carol, dave], amount, { from: alice }); 18 | // 检查 3 个账户的 Noah 币数量 19 | const bobBalance = await noahTokenInstance.balanceOf(bob); 20 | const carolBalance = await noahTokenInstance.balanceOf(carol); 21 | const daveBalance = await noahTokenInstance.balanceOf(dave); 22 | assert.equal(bobBalance.toString(), amount); 23 | assert.equal(carolBalance.toString(), amount); 24 | assert.equal(daveBalance.toString(), amount); 25 | // 检查空投合约的 Noah 币数量 26 | const airdropBalance = await noahTokenInstance.balanceOf(airdropInstance.address); 27 | assert.equal(airdropBalance.toString(), airdropTotalAmount - 3 * amount); 28 | }); 29 | 30 | it("oneToOne", async () => { 31 | // 发 Noah 币,发行 1024 个 32 | const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice }); 33 | // 发空投合约 34 | const airdropInstance = await Airdrop.new(noahTokenInstance.address, { from: alice }); 35 | // 给空投合约转账 100 个 Noah 币 36 | const airdropTotalAmount = 100; 37 | await noahTokenInstance.transfer(airdropInstance.address, airdropTotalAmount, { from: alice }); 38 | // 给 3 个账户发空投,bob 10 个,carol 15 个,dave 20 个 39 | const amounts = [10, 15, 20]; 40 | await airdropInstance.oneToOne([bob, carol, dave], amounts, { from: alice }); 41 | // 检查 3 个账户的 Noah 币数量 42 | const bobBalance = await noahTokenInstance.balanceOf(bob); 43 | const carolBalance = await noahTokenInstance.balanceOf(carol); 44 | const daveBalance = await noahTokenInstance.balanceOf(dave); 45 | assert.equal(bobBalance.toString(), amounts[0]); 46 | assert.equal(carolBalance.toString(), amounts[1]); 47 | assert.equal(daveBalance.toString(), amounts[2]); 48 | // 检查空投合约的 Noah 币数量 49 | const airdropBalance = await noahTokenInstance.balanceOf(airdropInstance.address); 50 | assert.equal(airdropBalance.toString(), airdropTotalAmount - amounts.reduce((a, b) => a + b)); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /contract/noah-token/test/faucet.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | const Faucet = artifacts.require("Faucet"); 3 | 4 | contract("Faucet", (accounts) => { 5 | const [alice, bob] = accounts; 6 | 7 | it("withdraw", async () => { 8 | // 发 Noah 币,发行 100 个 9 | const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice }); 10 | // 发水龙头,每次发 1 个 11 | const faucetInstance = await Faucet.new(noahTokenInstance.address, 1, { from: alice }); 12 | // 把 100 个 Noah 币转给水龙头 13 | await noahTokenInstance.transfer(faucetInstance.address, 100, { from: alice }); 14 | // 查看水龙头的余额 15 | const res = await noahTokenInstance.balanceOf(faucetInstance.address, { from: alice }); 16 | assert.equal(res.words[0], 100, "水龙头的余额不是 100"); 17 | // bob 从水龙头取币 18 | await faucetInstance.withdraw({ from: bob }); 19 | try { 20 | // bob 重复从水龙头取币 21 | await faucetInstance.withdraw({ from: bob }); 22 | } catch (e) { 23 | // 重复取币会报错 24 | assert.include(e.message, 'You can only request tokens once every 24 hours', 'bob 不能重复取币') 25 | } 26 | // 查看水龙头的余额 27 | const res2 = await noahTokenInstance.balanceOf(faucetInstance.address, { from: alice }); 28 | assert.equal(res2.words[0], 99, "水龙头的余额不是 99"); 29 | // 查看 bob 的余额 30 | const res3 = await noahTokenInstance.balanceOf(bob, { from: alice }); 31 | assert.equal(res3.words[0], 1, "bob 的余额不是 1"); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /contract/noah-token/test/token.js: -------------------------------------------------------------------------------- 1 | const NoahToken = artifacts.require("NoahToken"); 2 | 3 | contract("Token", (accounts) => { 4 | const [alice, bob] = accounts; 5 | 6 | it("balanceOf", async () => { 7 | // 发 Noah 币,发行 1024 个 8 | const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice }); 9 | // 查看 alice 的余额是否是 1024 10 | const result = await noahTokenInstance.balanceOf(alice); 11 | assert.equal(result.valueOf().words[0], 1024, "1024 wasn't in alice"); 12 | }); 13 | 14 | it("transfer", async () => { 15 | // 发 Noah 币,发行 1024 个 16 | const noahTokenInstance = await NoahToken.new('noah', 'NOAH', 0, '1024', { from: alice }); 17 | // alice 将 1 个 Noah 币转给 bob 18 | await noahTokenInstance.transfer(bob, 1, { from: alice }); 19 | // 查看 alice 的余额是否是 1023 20 | let aliceBalanceResult = await noahTokenInstance.balanceOf(alice); 21 | assert.equal(aliceBalanceResult.valueOf().words[0], 1023, "1023 wasn't in alice"); 22 | // 查看 bob 的余额是否是 1 23 | let bobBalanceResult = await noahTokenInstance.balanceOf(bob); 24 | assert.equal(bobBalanceResult.valueOf().words[0], 1, "1 wasn't in bob"); 25 | 26 | // bob 将 1 个 Noah 币转给 alice 27 | await noahTokenInstance.transfer(alice, 1, { from: bob }); 28 | // 查看 alice 的余额是否是 1024 29 | aliceBalanceResult = await noahTokenInstance.balanceOf(alice); 30 | assert.equal(aliceBalanceResult.valueOf().words[0], 1024, "1024 wasn't in alice"); 31 | // 查看 bob 的余额是否是 0 32 | bobBalanceResult = await noahTokenInstance.balanceOf(bob); 33 | assert.equal(bobBalanceResult.valueOf().words[0], 0, "0 wasn't in bob"); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /contract/noah-token/truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation, and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * https://trufflesuite.com/docs/truffle/reference/configuration 10 | * 11 | * Hands-off deployment with Infura 12 | * -------------------------------- 13 | * 14 | * Do you have a complex application that requires lots of transactions to deploy? 15 | * Use this approach to make deployment a breeze 🏖️: 16 | * 17 | * Infura deployment needs a wallet provider (like @truffle/hdwallet-provider) 18 | * to sign transactions before they're sent to a remote public node. 19 | * Infura accounts are available for free at 🔍: https://infura.io/register 20 | * 21 | * You'll need a mnemonic - the twelve word phrase the wallet uses to generate 22 | * public/private key pairs. You can store your secrets 🤐 in a .env file. 23 | * In your project root, run `$ npm install dotenv`. 24 | * Create .env (which should be .gitignored) and declare your MNEMONIC 25 | * and Infura PROJECT_ID variables inside. 26 | * For example, your .env file will have the following structure: 27 | * 28 | * MNEMONIC = 29 | * PROJECT_ID = 30 | * 31 | * Deployment with Truffle Dashboard (Recommended for best security practice) 32 | * -------------------------------------------------------------------------- 33 | * 34 | * Are you concerned about security and minimizing rekt status 🤔? 35 | * Use this method for best security: 36 | * 37 | * Truffle Dashboard lets you review transactions in detail, and leverages 38 | * MetaMask for signing, so there's no need to copy-paste your mnemonic. 39 | * More details can be found at 🔎: 40 | * 41 | * https://trufflesuite.com/docs/truffle/getting-started/using-the-truffle-dashboard/ 42 | */ 43 | require('dotenv').config({ 44 | path: './.env.local' 45 | }); 46 | 47 | const { PRIVATE_KEY, PROJECT_ID } = process.env; 48 | 49 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 50 | 51 | module.exports = { 52 | /** 53 | * Networks define how you connect to your ethereum client and let you set the 54 | * defaults web3 uses to send transactions. If you don't specify one truffle 55 | * will spin up a managed Ganache instance for you on port 9545 when you 56 | * run `develop` or `test`. You can ask a truffle command to use a specific 57 | * network from the command line, e.g 58 | * 59 | * $ truffle test --network 60 | */ 61 | 62 | networks: { 63 | // Useful for testing. The `development` name is special - truffle uses it by default 64 | // if it's defined here and no other network is specified at the command line. 65 | // You should run a client (like ganache, geth, or parity) in a separate terminal 66 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 67 | // options below to some value. 68 | // 69 | development: { 70 | host: "127.0.0.1", // Localhost (default: none) 71 | port: 7545, // Standard Ethereum port (default: none) 72 | network_id: "*", // Any network (default: none) 73 | }, 74 | // 75 | // An additional network, but with some advanced options… 76 | // advanced: { 77 | // port: 8777, // Custom port 78 | // network_id: 1342, // Custom network 79 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 80 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 81 | // from:
, // Account to send transactions from (default: accounts[0]) 82 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 83 | // }, 84 | // 85 | // Useful for deploying to a public network. 86 | // Note: It's important to wrap the provider as a function to ensure truffle uses a new provider every time. 87 | goerli: { 88 | provider: () => new HDWalletProvider(PRIVATE_KEY, `https://goerli.infura.io/v3/${PROJECT_ID}`), 89 | network_id: 5, // Goerli's id 90 | confirmations: 2, // # of confirmations to wait between deployments. (default: 0) 91 | timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 92 | skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 93 | }, 94 | // 95 | // Useful for private networks 96 | // private: { 97 | // provider: () => new HDWalletProvider(MNEMONIC, `https://network.io`), 98 | // network_id: 2111, // This network is yours, in the cloud. 99 | // production: true // Treats this network as if it was a public net. (default: false) 100 | // } 101 | }, 102 | 103 | // Set default mocha options here, use special reporters, etc. 104 | mocha: { 105 | // timeout: 100000 106 | }, 107 | 108 | // Configure your compilers 109 | compilers: { 110 | solc: { 111 | version: "0.8.17" // Fetch exact version from solc-bin (default: truffle's version) 112 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 113 | // settings: { // See the solidity docs for advice about optimization and evmVersion 114 | // optimizer: { 115 | // enabled: false, 116 | // runs: 200 117 | // }, 118 | // evmVersion: "byzantium" 119 | // } 120 | } 121 | } 122 | 123 | // Truffle DB is currently disabled by default; to enable it, change enabled: 124 | // false to enabled: true. The default storage location can also be 125 | // overridden by specifying the adapter settings, as shown in the commented code below. 126 | // 127 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 128 | // make a backup of your artifacts to a safe location before enabling this feature. 129 | // 130 | // After you backed up your artifacts you can utilize db by running migrate as follows: 131 | // $ truffle migrate --reset --compile-all 132 | // 133 | // db: { 134 | // enabled: false, 135 | // host: "127.0.0.1", 136 | // adapter: { 137 | // name: "indexeddb", 138 | // settings: { 139 | // directory: ".db" 140 | // } 141 | // } 142 | // } 143 | }; 144 | -------------------------------------------------------------------------------- /contract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contract", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Noah", 11 | "license": "ISC", 12 | "dependencies": { 13 | "ethers": "^5.7.2", 14 | "fs-extra": "^11.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /contract/utils/deploy.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const path = require("path"); 3 | 4 | function copyAbiFile(projectName) { 5 | const srcPath = path.resolve(__dirname, `../${projectName}/build/contracts/`) 6 | const destPath = path.resolve(__dirname, `../../frontend/abi/`) 7 | const res = fs.copySync(srcPath, destPath, { 8 | overwrite: true, 9 | }) 10 | } 11 | 12 | function updateEnvFile(key, value) { 13 | const envPath = path.resolve(__dirname, '../../frontend/.env.local') 14 | const envFile = fs.readFileSync(envPath, 'utf-8') 15 | let env = envFile.toString() 16 | env = env.replace(new RegExp(`NEXT_PUBLIC_${key}=.*\n`, 'g'), 17 | `NEXT_PUBLIC_${key}=${value}\n`) 18 | fs.writeFileSync(envPath, env); 19 | } 20 | 21 | module.exports = { 22 | copyAbiFile, 23 | updateEnvFile, 24 | } -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | # ! 开头的是必填项 2 | 3 | # 你的 alchemy api key(如果本地有区块链环境则不需要配置) 4 | NEXT_PUBLIC_ALCHEMY_API_KEY="" 5 | # !你的 token 合约地址 6 | NEXT_PUBLIC_TOKEN_CONTRACT_ADDRESS="" 7 | # !你的 faucet 合约地址 8 | NEXT_PUBLIC_FAUCET_CONTRACT_ADDRESS="" 9 | # !你的 airdrop 合约地址 10 | NEXT_PUBLIC_AIRDROP_CONTRACT_ADDRESS="" 11 | # !你的 NFT 合约地址 12 | NEXT_PUBLIC_NFT_CONTRACT_ADDRESS="" 13 | 14 | # 以下配置用于后端功能,如果不需要后端功能则不需要配置 15 | # 后端功能包括: 16 | # 水龙头 17 | 18 | # 你的 json rpc url,如果是本地 ganache 环境则填写 http://localhost:7545 19 | JSON_RPC_URL="" 20 | # 你的 chain id,如果是本地 ganache 环境则填写 1337 21 | CHAIN_ID= 22 | # 你的钱包私钥,用于水龙头转账 23 | WALLET_PRIVATE_KEY="" 24 | # 你的 google auth id,用于 google 登录 25 | GOOGLE_CLIENT_ID="" 26 | # 你的 google auth secret,用于 google 登录 27 | GOOGLE_CLIENT_SECRET="" 28 | # 你的 github auth id,用于 github 登录 29 | GITHUB_ID="" 30 | # 你的 github auth secret,用于 github 登录 31 | GITHUB_SECRET="" 32 | # nextauth 的 url,本地填写 http://localhost:4322 33 | NEXTAUTH_URL="" 34 | # nextauth 的 secret,随意填写 35 | NEXTAUTH_SECRET="" 36 | # 你的数据库 url 37 | DATABASE_URL="" 38 | # 你的 shadow 数据库 url,如果是本地数据库则不需要配置 39 | SHADOW_DATABASE_URL="" 40 | # !如果是本地环境则填写 development,如果是生产环境则填写 production 41 | NEXT_PUBLIC_ENV="development" 42 | # 你的 pinata api key https://www.pinata.cloud/ 43 | PINATA_API_KEY= 44 | # 你的 pinata api secret https://www.pinata.cloud/ 45 | PINATA_API_SECRET= 46 | # 你的 qstash public key,用来同步 nft 数据 https://console.upstash.com/qstash 47 | QSTASH_PK= 48 | # 你的 Google Analytics ID,用来统计网站访问量 49 | NEXT_PUBLIC_GA_ID= -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /frontend/abi/IERC165.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "IERC165", 3 | "abi": [ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "bytes4", 8 | "name": "interfaceId", 9 | "type": "bytes4" 10 | } 11 | ], 12 | "name": "supportsInterface", 13 | "outputs": [ 14 | { 15 | "internalType": "bool", 16 | "name": "", 17 | "type": "bool" 18 | } 19 | ], 20 | "stateMutability": "pure", 21 | "type": "function" 22 | } 23 | ], 24 | "metadata": "{\"compiler\":{\"version\":\"0.8.17+commit.8df45f5f\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"project:/contracts/IERC165.sol\":\"IERC165\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[]},\"sources\":{\"project:/contracts/IERC165.sol\":{\"keccak256\":\"0x5101b6ad6c3a2d50f1b8060902ee4dfa55e6bbedf3c0568bc0fcc03eb4a9a316\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://9b55436690d3d9fa61ac8922cad2b1a57a1b07830a38ae8022d86003d6c30ac9\",\"dweb:/ipfs/QmPBi9z2WdgKYAiCnAThaCmBLXk2znyyAncHFjkHEMhQwp\"]}},\"version\":1}", 25 | "bytecode": "0x", 26 | "deployedBytecode": "0x", 27 | "immutableReferences": {}, 28 | "generatedSources": [], 29 | "deployedGeneratedSources": [], 30 | "sourceMap": "", 31 | "deployedSourceMap": "", 32 | "source": "// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.0;\n\ninterface IERC165 {\n function supportsInterface(bytes4 interfaceId) external pure returns (bool);\n}\n", 33 | "sourcePath": "/Users/luzhenqian/Work/web3/web3-examples/contract/noah-nft/contracts/IERC165.sol", 34 | "ast": { 35 | "absolutePath": "project:/contracts/IERC165.sol", 36 | "exportedSymbols": { 37 | "IERC165": [ 38 | 9 39 | ] 40 | }, 41 | "id": 10, 42 | "license": "MIT", 43 | "nodeType": "SourceUnit", 44 | "nodes": [ 45 | { 46 | "id": 1, 47 | "literals": [ 48 | "solidity", 49 | "^", 50 | "0.8", 51 | ".0" 52 | ], 53 | "nodeType": "PragmaDirective", 54 | "src": "33:23:0" 55 | }, 56 | { 57 | "abstract": false, 58 | "baseContracts": [], 59 | "canonicalName": "IERC165", 60 | "contractDependencies": [], 61 | "contractKind": "interface", 62 | "fullyImplemented": false, 63 | "id": 9, 64 | "linearizedBaseContracts": [ 65 | 9 66 | ], 67 | "name": "IERC165", 68 | "nameLocation": "68:7:0", 69 | "nodeType": "ContractDefinition", 70 | "nodes": [ 71 | { 72 | "functionSelector": "01ffc9a7", 73 | "id": 8, 74 | "implemented": false, 75 | "kind": "function", 76 | "modifiers": [], 77 | "name": "supportsInterface", 78 | "nameLocation": "91:17:0", 79 | "nodeType": "FunctionDefinition", 80 | "parameters": { 81 | "id": 4, 82 | "nodeType": "ParameterList", 83 | "parameters": [ 84 | { 85 | "constant": false, 86 | "id": 3, 87 | "mutability": "mutable", 88 | "name": "interfaceId", 89 | "nameLocation": "116:11:0", 90 | "nodeType": "VariableDeclaration", 91 | "scope": 8, 92 | "src": "109:18:0", 93 | "stateVariable": false, 94 | "storageLocation": "default", 95 | "typeDescriptions": { 96 | "typeIdentifier": "t_bytes4", 97 | "typeString": "bytes4" 98 | }, 99 | "typeName": { 100 | "id": 2, 101 | "name": "bytes4", 102 | "nodeType": "ElementaryTypeName", 103 | "src": "109:6:0", 104 | "typeDescriptions": { 105 | "typeIdentifier": "t_bytes4", 106 | "typeString": "bytes4" 107 | } 108 | }, 109 | "visibility": "internal" 110 | } 111 | ], 112 | "src": "108:20:0" 113 | }, 114 | "returnParameters": { 115 | "id": 7, 116 | "nodeType": "ParameterList", 117 | "parameters": [ 118 | { 119 | "constant": false, 120 | "id": 6, 121 | "mutability": "mutable", 122 | "name": "", 123 | "nameLocation": "-1:-1:-1", 124 | "nodeType": "VariableDeclaration", 125 | "scope": 8, 126 | "src": "152:4:0", 127 | "stateVariable": false, 128 | "storageLocation": "default", 129 | "typeDescriptions": { 130 | "typeIdentifier": "t_bool", 131 | "typeString": "bool" 132 | }, 133 | "typeName": { 134 | "id": 5, 135 | "name": "bool", 136 | "nodeType": "ElementaryTypeName", 137 | "src": "152:4:0", 138 | "typeDescriptions": { 139 | "typeIdentifier": "t_bool", 140 | "typeString": "bool" 141 | } 142 | }, 143 | "visibility": "internal" 144 | } 145 | ], 146 | "src": "151:6:0" 147 | }, 148 | "scope": 9, 149 | "src": "82:76:0", 150 | "stateMutability": "pure", 151 | "virtual": false, 152 | "visibility": "external" 153 | } 154 | ], 155 | "scope": 10, 156 | "src": "58:102:0", 157 | "usedErrors": [] 158 | } 159 | ], 160 | "src": "33:128:0" 161 | }, 162 | "compiler": { 163 | "name": "solc", 164 | "version": "0.8.17+commit.8df45f5f.Emscripten.clang" 165 | }, 166 | "networks": {}, 167 | "schemaVersion": "3.4.11", 168 | "updatedAt": "2023-01-10T18:46:09.556Z", 169 | "devdoc": { 170 | "kind": "dev", 171 | "methods": {}, 172 | "version": 1 173 | }, 174 | "userdoc": { 175 | "kind": "user", 176 | "methods": {}, 177 | "version": 1 178 | } 179 | } -------------------------------------------------------------------------------- /frontend/components/Gtag.tsx: -------------------------------------------------------------------------------- 1 | import { GA_TRACKING_ID, pageView } from "@/libs/gtag"; 2 | import { useRouter } from "next/router"; 3 | import Script from "next/script"; 4 | import { useEffect } from "react"; 5 | 6 | function GTag() { 7 | const router = useRouter(); 8 | useEffect(() => { 9 | const handleRouteChange = (url: string) => { 10 | pageView(url); 11 | }; 12 | router.events.on("routeChangeComplete", handleRouteChange); 13 | router.events.on("hashChangeComplete", handleRouteChange); 14 | return () => { 15 | router.events.off("routeChangeComplete", handleRouteChange); 16 | router.events.off("hashChangeComplete", handleRouteChange); 17 | }; 18 | }, [router.events]); 19 | return ( 20 | <> 21 |