\n \u003C/ClientOnly>\n\u003C/template>\n\n\u003Cscript setup>\nconst model = ref()\nconst toolbarsConfig = {\n // ... toolbars config\n}\n\u003C/script>\n\n4. 上传图片\n//增加两个参数: md、@imgAdd\n\u003Cmavon-editor v-model=\"content\" ref=\"md\" @imgAdd=\"upImage\" /> \u003Cbr>\n\u003Cscript setup>\n let md = ref()\n const readFile = (file) => {\n return new Promise(resolve => {\n let reader = new FileReader()\n reader.readAsArrayBuffer(file)\n reader.onload = (event) => {\n resolve(Buffer.from(event.target.result))\n }\n })\n\n}\nconst upImage = async(pos, file) => {\n //换成自己的ipfs主机,或是使用自己的上传逻辑\n const ipfs = create({ host: 'ipfs.example.io', port: '2885', protocol: 'https' })\n const ipfs_host = \"https://ipfs.example.io/ipfs/\" \n let content = await readFile(file)\n let res = await ipfs.add(content)\n md.value.$img2Url(pos, ipfs_host+res.path)\n}\n\u003C/script>\n```\n\n好了,集成好了。`manon-editor`是我用了很长的编辑器,都有感情了,舍不得换其它的。再说它也很好嵌入和易用,还是多想办法集成为是。\n\n",[15,16,198,199,218,219],"manoneditor","editor","2025-08-02T02:28:33.000Z",{"_id":222,"user_id":8,"username":9,"title":223,"author":9,"category":11,"permlink":224,"body":225,"tags":226,"created":229,"__v":23},"68a4297e9c0586aeab990978","Supervisor管理python web服务 / 网络研习社#88","hp1qg1vb","\n\nSupervisor就是用Python开发的一套通用的进程管理程序,能将一个普通的命令行进程变为后台daemon,并监控进程状态,异常退出时能自动重启。\n\n因为是python开发的一个库,可以直接用pip来安装,很方便。supervisor安装完成后会生成三个执行程序:supervisord、supervisorctl、echo_supervisord_conf,分别是supervisor的守护进程服务(用于接收进程管理命令)、客户端(用于和守护进程通信,发送管理进程的指令)、生成初始配置文件程序。\n\n```py\npip install supervisor #supervisor-4.2.5\n\necho_supervisord_conf #查看基本配置\necho_supervisord_conf > /home/wsgi.ini #生成初始配置文件\n\n# 配置范例\n[program:wsgiX] ;program:名称\n;工作目录(脚本启动目录的全路径)\ndirectory=/home/knowqa \n;启动命令,当然你可以直接 python api.py,此处使用gunicorn启动\ncommand = /home/knowqa/know_env/bin/python /home/knowqa/know_env/bin/gunicorn -c config.py api:app\nstartsecs=0\nstopwaitsecs=0\nautostart=true ;supervisord守护程序启动时自动启动tornado\nautorestart=true ;supervisord守护程序重启时自动重启tornado\nredirect_stderr=true ;将stderr重定向到stdout\n;日志标准输出路径,同时脚本print打印信息也会在改文件显示\nstdout_logfile=./stdout.log\nstderr_logfile=./error.log\n\n;守护进程,可在 web 上访问\n[inet_http_server] ; inet (TCP) server disabled by default\nport=0.0.0.0:9001 ; (ip_address:port specifier, *:port for all iface) 127.0.0.1\nusername=user ; (default is no username (open server))\npassword=123 ; (default is no password (open server))\n\n;supervisord日志配置\n[supervisord]\nlogfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log\nlogfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB\nlogfile_backups=1\n\n# 启动\nsupervisord -c ./wsgi.ini\n//supervisord -c /home/wsgi.ini\n```\n\n### 查看运行状态\n`ps -ef | grep supervisord`\n\n\n看到如上所示,即运行正常。如果要停止,则直接kill, 比如: `kill 197320` 。命令如下:\nkill pid #停止运行\n\n在Supervisor的使用中,**其中“工作目录”和 “启动命令”是最关键的两处设置,务必正确!** 在“启动命令”时可以看到是使用`gunicorn`来启动服务的,就来补下`gunicorn`的设置。\n\n## Gunicorn\nGunicorn是一个unix上被广泛使用的高性能的Python WSGI UNIX HTTP Server,和大多数的web框架兼容,并具有实现简单,轻量级,高性能等特点。\n\n```py\npip install gunicorn\n\n# 快速启动run.py\ngunicorn --workers=4 --bind=0.0.0.0:8000 run:app\n\n# run.py\nfrom flask import Flask\napp = Flask(__name__)\n\n# 配置文件启动\n命令行中定义的参数,都可以放在配置文件中。\n# 配置文件范例 config.py\nimport multiprocessing\n\nbind = \"0.0.0.0:8000\"\nworkers = multiprocessing.cpu_count() * 2 + 1\n\nbacklog = 2048\nworker_class = \"eventlet\"\nworker_connections = 1000\ndaemon = True\npidfile = 'log/gunicorn.pid'\naccesslog = 'log/access.log'\nerrorlog = 'log/gunicorn.log'\n\n# 启动\ngunicorn --config=config.py run:app\n\n# 服务重启、退出等\n获取Gunicorn进程树,用下面的命令获取gunicorn的Master PID\n#方法1\npstree -ap|grep gunicorn\n#方法2\nps -ef|grep gunicorn \n# 重启Gunicorn进程\nHUP(终端断线)信号发出之后,worker进程会进行被杀掉,并启动新的进程,保证源代码的修改会反映进来。master进程不会变。\nkill -HUP master_pid\n# 优雅停止Gunicorn进程\npkill gunicorn\nkill -9 master_pid\n```\n\n\n\n\n",[15,198,16,227,173,228],"gunicorn","python","2024-04-28T06:35:51.000Z",{"_id":231,"user_id":8,"username":9,"title":232,"author":9,"category":11,"permlink":233,"body":234,"tags":235,"created":240,"__v":23},"68a4297e9c0586aeab990926","sharp分割图片 / 网络研习社#87","i045jyle","最近在开发AI绘图时,Midjourney生成图像时是一次就是四张,并且这四张是长一起的!分析了一下这张图片,其实这并不是一张小图,而是一张大图(7M以上)!它其实就是成品,并不需要额外地再去放大生成一次!discord中要再次生成,估计也是想要你多消费吧。获取的数据如下:\n```js\n{\n id: '1186583581955469334',\n flags: 0,\n content: '**futuristic motorcycle concept inspired by Egyptian mythology** - \u003C@901742107700633620> (fast)',\n hash: 'cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189',\n progress: 'done',\n uri: 'https://cdn.discordapp.com/attachments/1073862082413473814/1186583581678653511/lemooljiang_futuristic_motorcycle_concept_inspired_by_Egyptian__cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189.png?ex=6593c713&is=65815213&hm=0eb95c361d276f887d9b7e59876a2cd9d6b2edcda15653005e2786df09f40b09&',\n proxy_url: 'https://media.discordapp.net/attachments/1073862082413473814/1186583581678653511/lemooljiang_futuristic_motorcycle_concept_inspired_by_Egyptian__cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189.png?ex=6593c713&is=65815213&hm=0eb95c361d276f887d9b7e59876a2cd9d6b2edcda15653005e2786df09f40b09&',\n options: [\n {\n type: 2,\n style: 2,\n label: 'U1',\n custom: 'MJ::JOB::upsample::1::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189'\n },\n {\n type: 2,\n style: 2,\n label: 'U2',\n custom: 'MJ::JOB::upsample::2::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189'\n },\n {\n type: 2,\n style: 2,\n label: 'U3',\n custom: 'MJ::JOB::upsample::3::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189'\n },\n {\n type: 2,\n style: 2,\n label: 'U4',\n custom: 'MJ::JOB::upsample::4::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189'\n },\n {\n type: 2,\n style: 2,\n label: '',\n custom: 'MJ::JOB::reroll::0::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189::SOLO'\n },\n {\n type: 2,\n style: 2,\n label: 'V1',\n custom: 'MJ::JOB::variation::1::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189'\n },\n {\n type: 2,\n style: 2,\n label: 'V2',\n custom: 'MJ::JOB::variation::2::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189'\n },\n {\n type: 2,\n style: 2,\n label: 'V3',\n custom: 'MJ::JOB::variation::3::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189'\n },\n {\n type: 2,\n style: 2,\n label: 'V4',\n custom: 'MJ::JOB::variation::4::cb0ebbd9-ab8f-472d-b9a6-8d42d9bc9189'\n }\n ],\n width: 2048,\n height: 2048\n}\n```\n\n\n\n\nMidjourney一次生成的图像\n\n从图中可以看到,图片的精度已经足够,没有心要再去单独生成放大图。至于变异图,再生成一次就好啦。好了,那摆在眼前的就是将这个拼图平均拆成四份,再上传到服务器,得到需要的url地址.\n\n这里用到的拆分的包是`sharp`, 拆分的图传到IPFS网络, 就这么个思路。\n\n## sharp使用\n```js\nnpm install sharp --save\n\nimport sharp from 'sharp'\n\nasync function splitImageIntoFour(inputImagePath) {\n try {\n // 读取图片信息\n const metadata = await sharp(inputImagePath).metadata()\n console.log(566, metadata)\n /*\n {\n format: 'png',\n width: 2048,\n height: 2048,\n space: 'srgb',\n channels: 3,\n depth: 'uchar',\n density: 72,\n isProgressive: false,\n hasProfile: false,\n hasAlpha: false\n }\n */\n\n // 计算每个部分的尺寸\n const width = metadata.width / 2\n const height = metadata.height / 2\n console.log(233, \"width height\", width, height)\n \n\n // 分别裁剪四个部分\n const topLeft = sharp(inputImagePath).extract({ left: 0, top: 0, width, height })\n await topLeft.toFile('./top-left.png') \n const topRight = sharp(inputImagePath).extract({ left: width, top: 0, width, height })\n await topRight.toFile('./top-right.png')\n const bottomLeft = sharp(inputImagePath).extract({ left: 0, top: height, width, height })\n await bottomLeft.toFile('./bottom-left.png')\n const bottomRight = sharp(inputImagePath).extract({ left: width, top: height, width, height })\n await bottomRight.toFile('./bottom-right.png')\n\n console.log('图片已成功四等分')\n\n } catch (error) {\n console.error('发生错误:', error)\n }\n}\n\n// 调用函数,传入图片路径\nsplitImageIntoFour('./image/test.png')\n\n//也可以直接传入网络图片,以buffer传入\nconst url = \"https://cdn.discordapp.com/attachments/107386208241ebb513ea501934aabf0a&\"\nconst response = await fetch(url)\nconst bufferX = await response.arrayBuffer()\nsplitImageIntoFour(bufferX)\n```\n\n## 上传网络图片到IPFS\n```js\nimport fetch from 'node-fetch'\n\nlet url = \"https://pic.ntimg.cn/file/20160921/4562496_103844736000_2.jpg\"\nconst response = await fetch(url)\nconst bufferX = await response.arrayBuffer()\nconst content = Buffer.from(bufferX)\nlet resX = await ipfs.add(content)\nlet imgHash = resX.path\nconsole.log(88, resX, 456, imgHash)\n```\n\n[AI·Joe](https://ai.ilark.io/image) 已新增了Midjourney模型,欢迎大家使用!\n\n\n",[15,16,198,236,237,238,239],"midjourney","sharp","ipfs","aigc","2023-12-28T03:22:03.000Z",{"_id":242,"user_id":8,"username":9,"title":243,"author":9,"category":11,"permlink":244,"body":245,"tags":246,"created":249,"__v":23},"68a4297e9c0586aeab9908ec","linux文件权限梳理 / 网络研习社#86","0ut1qme3","linux的文件权限颇为复杂,简单地读下资料是很难弄明白的,非得实践多次才能完全整明白。以前我是对权限不太重视,不管在哪,都是一个root通行天下。现在有多人协作,以及安全性方面的考虑,需要适当地避免root而使用普通用户的角色。\n\n文件权限是个系统工程,对于linux来说“一切皆文件”。,要梳理这方面的知识,也确实花了近两天的时间。整理和测试后,归纳如下:\n\n\n\n```sh\nll 查看当前所有目录的权限 \nll -d /home/test 查看对test目录的权限\n\ndrwxr-xr-x 4 root root 4096 May 31 09:52 test/\ndrwxr-xr-x 2 lemool lemool 4096 Sep 28 02:31 lemool/\ndrwxr-xr-x 2 root root 4096 Apr 6 13:27 mongodb/\n-rw-r--r-- 1 root root 68 Aug 28 07:01 ser.txt\n前十位表示文件或文件夹的权限,\n第一个数字表示文件类型,2-4 属主, 5-7 属组,8-10 其他人\n\n权限代号:\nr:读取权限,数字代号为 “4” \nw:写入权限,数字代号为 “2” \nx:执行权限,数字代号为 “1” \n-:不具备任何权限,数字代号为 “0”\n\n权限操作: \nchmod + - = 改变权限 u g o a\nchmod [ u / g / o / a ] [ + / - / = ] [ r / w / x ] file\n[ u / g / o / a ] 为权限范围,其中 \nu:User,即文件或目录的拥有者 \ng:Group,即文件或目录的所属群组 \no:Other,除了文件或目录拥有者和所属群组外,其他用户都属于这个范围 \na:All,即全部用户\n+表示增加权限 \n-表示取消权限 \n=表示取消之前的权限,并给予唯一的权限\neg:\n chmod u=rwx a.txt //对a.txt的属主增加所有权限\n chmod u+w a.txt //对a.txt的属主增加写权限\n chmod u=- a.txt //对a.txt的属主减去所有权限\n chmod o+w /home/test //对目录test增加其他人的可写权限\n chmod u+rw /code/readme.txt //给User用户增加了对”/code/readme.txt”文件 “w” 和 “x” 的权限\n chown lem.lem test.txt //更改test.txt的属主和属组为lem\n\n chmod +x 的意思就是给执行权限\n chmod +x start.sh\n\n# 也可以数字的形式来代表权限\n# r-4 w-2 x-1 --0 几个数字相加得到权限,比如7就是所有权限\nchmod o+w /home/test //增加其他人对test文件夹的权限,其他人就可以在此自由的操作了\nchmod -R 774 /code/ //修改这个目录,以及子目录下文件的所有权限, -R表示递归\nUser : 7 = 111 表示具有 ” r , w , x” 权限 \nGroup : 7 = 111 表示具有 ” r , w , x” 权限 \nOther : 4 = 100 表示只具有 ” r ” 权限,而没有 “w,x” 权限\n```\n\n总结下来看似简单,但要理解每个符号的意义却要花不少时间和心思。行动起来吧!\n",[15,16,198,247,248],"linux","chmod","2023-10-10T03:27:48.000Z",{"_id":251,"user_id":8,"username":9,"title":252,"author":9,"category":11,"permlink":253,"body":254,"tags":255,"created":257,"__v":23},"68a4297e9c0586aeab9908de","Mongodb数据库遍历 / 网络研习社#85","cfw6j6hj","\n\nhttps://www.mongodb.com/\n\nMongodb是个简单易用、拓展性极强的数据库。对它的查询和遍历说不上难,但也有些技巧在的。\n\n## 条件查询\n```js\n//增加查找条件\nor() :添加限制条件\nlimit(1) :查看几个\nsort() :排序 \nskip():跳过几个\nUser.find()//查询全部\n .or([{gender:2}])//查询gender为2的数据\n .sort({_id:'desc'})//倒序排列数据, 或sort({_id: -1}),asc正序 \n .limit(2)//展示两条\n .skip(2)//跳过前两个\n\neg:\nlet images = await User\n .find({user_id: req.user_id})\n .sort({_id:'desc'})\n .limit(50)\n```\n\n## 对数据库遍历\n```js\n// nodejs端\n//查询用户\napp.get('/getusers/:skip', async (req, res) => {\n try {\n let limit = 5\n let skip = req.params.skip\n // 主要是通过limit和skip来进行遍历\n // limit 和 skip 要保持一致\n let users= await User\n .find()\n .sort({_id:'desc'})\n .limit(limit)\n .skip(skip)\n res.status(200).send({\n users\n })\n\n } catch (err) {\n console.error(556, err)\n res.status(500).send('Something went wrong');\n }\n})\n\n// nuxt前端\nlet skip = 5\nlet skipNum = ref(0)\n//获取用户\nconst getHistory = async () => { \n const getOption = {\n method: \"GET\",\n baseURL\n }\n let { data, error } = await useFetch('/getusers/0', getOption)\n console.log(668, data.value.users)\n}\n\nif(process.client){\n getHistory()\n}\n\nconst more = async () => {\n console.log(563, \"skipNum\", skipNum.value)\n const getOption = {\n method: \"GET\",\n baseURL\n }\n skipNum.value += skip\n let { data, error } = await useFetch('/getusers/'+skipNum.value, getOption)\n if(error.value) {\n message.error(\"失败!\\n\"+error.value.data, { duration: 5e3 })\n return\n }\n if(data.value.users.length \u003C skip){\n console.log(\"没有更多数据了!\")\n moreFlag.value = false\n }\n}\n```\n\n这个遍历有点像接力赛,查完一段再查下一段。通过limit和skip来完成一段段的查询,效果还是很完美的。",[15,198,16,256],"mongodb","2023-09-12T03:58:03.000Z",{"_id":259,"user_id":8,"username":9,"title":260,"author":9,"category":11,"permlink":261,"body":262,"tags":263,"created":267,"__v":23},"68a4297e9c0586aeab99088a","HTTP 响应状态码 / 网络研习社#84","fxosvvq1","如果有朋友找你借钱,你会如何回复呢?你肯定会说:那得看情况啊。如果只是借个几百块应个急,那就二话不说转过去了。如果要借个几万,你估计得说你得想想。如果要借几百刀,你估计就得直接回绝了:没那么多钱啰!如果把这些回应放在服务器上,道理也差不多。\n\n\n\n在express做服务器时,有时响应的状态还蛮迷惑的,什么时候发200, 什么时候发404,什么时候发500呢?这些其实都是标准,找文件来参考下。\n\n[HTTP 响应状态码](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status) 这个文件基本都涉及了,好像还挺长的,找几个常用的。\n```js\nres.sendStatus(200); // 'OK'\nres.sendStatus(403); // 'Forbidden'\nres.sendStatus(404); // 'Not Found'\nres.sendStatus(422); // Unprocessable Entity 请求格式正确,但由于语义错误而无法遵循。\nres.sendStatus(500); // 'Internal Server Error'\n```\n\n日常中就是上面这五种情况,灵活组合就基本够用了。\n",[15,198,264,265,266],"express","status","http","2023-05-17T03:56:51.000Z",[269,281,288,297,306,314,323,333],{"_id":270,"user_id":8,"username":9,"title":271,"author":9,"category":11,"permlink":272,"body":273,"tags":274,"created":280,"__v":23},"68a4297e9c0586aeab990a26","简易计算UniswapV2的代币价格 / 学习智能合约#67","4u038ju7","\nhttps://app.uniswap.org/swap\n\n简易计算UniswapV2的代币价格,这算是一个补充吧。以前虽也看过V2的合约,一般也用不着自己去求。这次是要集成$Slime的充值,需要计算一下价格,就找来资料试着计算下。\n\nUniswapV2主要使用了 RouterV2 合约的两个方法,把它写成ABI,如下:\n```js\nconst routerAbi = [\n \"function getAmountsOut(uint amountIn, address[] memory path) public view returns (uint[] memory amounts)\",\n \"function getAmountsIn(uint amountOut, address[] memory path) public view returns (uint[] memory amounts)\" \n ]\n```\n以第一个函数getAmountsOut为例,amountIn 表示卖出 token 数量,path 表示交易路径,比如你要卖出 TokenA,换成 ETH,那么最简单的交易路径就是 TokenA -> WETH, 返回值 amounts,表示能获得的目标 WETH 数量。所以 `getAmountsOut` 这个函数最终表达的意思就是:用某个数量的TokenA,能换到多少数量的TokenB。\n\n这里我们需要用一定量的$Slime换成WETH,然后再相除就可求出 $Slime/WETH的价格,代码如下:\n```js\nimport { ethers } from 'ethers'\n\n//Base网络配置\nconst base_url = \"https://mainnet.base.org\"\nconst routerAbi = [\n \"function getAmountsIn(uint amountOut, address[] memory path) public view returns (uint[] memory amounts)\",\n \"function getAmountsOut(uint amountIn, address[] memory path) public view returns (uint[] memory amounts)\"\n ]\nconst slimeCon = '0x68503A15efD0D2F81D185a07d60Ed9Ac2a66B59e'\nconst WETH = '0x4200000000000000000000000000000000000006'\nconst uniswapRouter = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24'\nconst provider = new ethers.JsonRpcProvider(base_url)\nconst routerContract = new ethers.Contract(uniswapRouter, routerAbi, provider) \n\nasync function getSlimePrice(){\n let setAmount = ethers.parseEther(\"28800\") //假设卖出28800个slime币\n let amountOuts = await routerContract.getAmountsOut(setAmount, [slimeCon, WETH])\n let amountEthOut = ethers.formatEther(amountOuts[1])\n console.log(896,setAmount, amountEthOut) \n //28800.0 0.001115090084330875\n // 最后价格\n let price_slime_eth = amountEthOut / 28800 \n}\n```\n\n得到slime/eth价格后再乘以以太的美元价格($3111.78),就得出$slime的美元价格:0.00012048\n\n",[15,16,275,276,277,278,279],"smartcontract","eth","ethers","vuejs","solidity","2024-11-17T03:55:12.000Z",{"_id":282,"user_id":8,"username":9,"title":283,"author":9,"category":11,"permlink":284,"body":285,"tags":286,"created":287,"__v":23},"68a4297e9c0586aeab990908","ethers.js与合约交互及事件机制 / 学习智能合约#66","suwqgwo8","上次测试了ethers.js与链上的交互功能,这次测试下与合约的交互以及查看事件的机制。这次也是准备打通链上与[AI·Joe](https://ai.ilark.io)的功能:充值与NFT。预备着过几天上线新功能。\n\n\n\nhttps://docs.ethers.org/v6/\n\n## 与合约交互\n[交互基础](https://learnblockchain.cn/article/5209)\n[ethers.js分析](https://learnblockchain.cn/article/5604)\n```js\n//只读方法 (like view and pure)\nimport { ethers } from 'ethers'\n//const url = \"http://127.0.0.1:8545\"\n// const url = \"https://polygon-rpc.com\"\nconst url = \"https://rpc-mumbai.maticvigil.com\"\nconst provider = new ethers.JsonRpcProvider(url)\nlet abi = [\n \"function str() view returns (string)\",\n \"function set(uint x)\",\n \"function get()view returns (uint)\"\n ]\n//这里的abi相当于接口\n// 也可以使用合约的编译abi: import Storage from '@/static/Storage.json' -> Storage.abi\nlet contractAddr = \"0x600a00aE84b896edd9F2732565cf2195d032315E\"\nlet contract = new ethers.Contract(contractAddr, abi, provider)\nlet str = await contract.get()\nconsole.log(256, str)\n\n//合约的所有方法, signer签名 metamask\nconst url = \"http://127.0.0.1:8545\"\nconst contractAddr = \"0x600a00aE84b896edd9F2732565cf2195d032315E\"\nconst provider = new ethers.JsonRpcProvider(url)\nconst signer = await provider.getSigner()\nconst contract = new ethers.Contract( contractAddr, Storage.abi, signer)\nconsole.log(56, \"signer:\", signer)\nconst option = { \n}\nconst tx = await contract.set(996, option)\nawait tx.wait()\nconsole.log(669, \"singen res:\", tx)\n\n//合约的set方法, 需要连上钱包来实现\nimport Storage from '@/static/Storage.json'\nconst url = \"http://127.0.0.1:8545\"\nconst contractAddr = \"0x600a00aE84b896edd9F2732565cf2195d032315E\"\nconst provider = new ethers.JsonRpcProvider(url)\nconst contract = new ethers.Contract(contractAddr, Storage.abi, provider)\nconst PRIVATE_KEY = \"0x182xxxxxxxxx\"\nconst wallet = new ethers.Wallet(PRIVATE_KEY, provider)\nconsole.log(56, \"wallet:\", wallet)\nconst nonce = await provider.getTransactionCount(wallet.address)\nconsole.log(58, \"nonce:\", nonce)\nconst StorageConnected = contract.connect(wallet)\nconst option = {\n nonce\n}\nconst tx = await StorageConnected.set(86394, option)\nawait tx.wait()\nconsole.log(669, \"res:\", tx)\n\n//另一种写法 在创建合约实例时导入钱包\nconst privateKey = 'bde95xxxxx'\nconst wallet = new ethers.Wallet(privateKey, provider)\nconsole.log(56, \"wallet:\", wallet)\nconst _Contract = new ethers.Contract(contractAddr, ContractAbi, wallet)\n\nconst main = async () => {\n //显示原本的 owner\n let ownerVal = await _Contract.getOwner()\n console.log(\"原本的 owner:\", ownerVal)\n //调用 setOwner 更改 owner\n await _Contract.setOwner(\"0x503063dD8f114059B09FD5bC953E71fc14a1d672\")\n //更改后的 owner\n ownerVal = await _Contract.getOwner()\n console.log(\"更改后的 owner:\", ownerVal)\n}\nmain()\n```\n\n## 友好的abi\n在上一点中,我们使用的 abi 并不是非常友好,咱们编写 abi 还有一种比较简单的 函数签名的方式 编写abi:\n```js\nconst ContractAbi = [\n \"function getOwner()view public returns(address)\",\n]\n其实简单点来说你就把函数签名进行复制过来就ok了,由于我们只是一个只读合约,复制一个只读方法即可,这样也可以完成上一点的内容。\neg:\nconst abi = [\n \"event Transfer(address indexed from, address indexed to, uint value)\",\n \"function balanceOf(address account) external view returns (uint256)\",\n \"function transfer(address recipient, uint256 amount) external returns (bool)\"\n ]\n```\n\n## 读取合约历史事件\n查询合约的历史数据通常涉及到查看合约发出的事件(events),queryFilter来查询\n```js\nimport { ethers } from \"ethers\"\n\nconst url = \"https://rpc-mumbai.maticvigil.com\"\nconst provider = new ethers.JsonRpcProvider(url)\nconst abi = [\n \"event Transfer(address indexed from, address indexed to, uint value)\"\n]\nconst address = '0xaA61b68301278Da4e001d2cF7F8b2202aed6347c'\nconst contract = new ethers.Contract(address, abi, provider)\n\n// Query the last 1000 blocks for any transfer\nlet filter = contract.filters.Transfer\nlet events = await contract.queryFilter(filter, -1000) \n//也可以这样写\n// const height = await provider.getBlockNumber()\n// const events = await contract.queryFilter('Transfer', height - 1000, height)\nconsole.log(899, \"events\", events)\n//\n[\n EventLog {\n provider: JsonRpcProvider {},\n transactionHash: '0x3ca891c7df6ce814e7a418bf1b6985ff88ec2087f540665c752a1916e0982e86',\n blockHash: '0xd8b47357bfcf1ae0185e458d393a1a8ba9a19999aa7c16c719edaadc30ad0419',\n blockNumber: 42621522,\n removed: false,\n address: '0xaA61b68301278Da4e001d2cF7F8b2202aed6347c',\n data: '0x00000000000000000000000000000000000000000000000ae2a8e81db2700000',\n topics: [\n '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',\n '0x000000000000000000000000654efb5135e61f3db91dd69d239c2e2812d73604',\n '0x000000000000000000000000469d135bfdde122a198b978d1a85f05100217246'\n ],\n index: 83,\n transactionIndex: 36,\n interface: Interface {\n fragments: [Array],\n deploy: [ConstructorFragment],\n fallback: null,\n receive: false\n },\n fragment: EventFragment {\n type: 'event',\n inputs: [Array],\n name: 'Transfer',\n anonymous: false\n },\n args: Result(3) [\n '0x654Efb5135E61f3Db91dd69d239c2E2812D73604',\n '0x469D135bFdDe122a198B978d1A85f05100217246',\n 200800000000000000000n\n ]\n }\n]\n```\n\n## 获取历史日志\ngetLogs方法获取历史日志\n```js\nlet contractEnsName = '0xaA61b68301278Da4e001d2cF7F8b2202aed6347c'\n\nlet topic = ethers.id(\"Transfer(address,address,uint256)\")\nconsole.log(123, topic)\nlet filter = {\n address: contractEnsName,\n fromBlock: -600,\n toBlock: 'latest',\n topics: [ topic ]\n}\n\nlet result = await provider.getLogs(filter)\nconsole.log(6663, result)\n//\n[ Log {\n provider: JsonRpcProvider {},\n transactionHash: '0x8736cd74cb13488df6b49899cd8833206e003c37f02560a92c0ac9495c7155c5',\n blockHash: '0x5e163f0a62cdc8c858a4aaf2ba6cf471c9abf074fc887d611db64a5005fa8a18',\n blockNumber: 42625293,\n removed: false,\n address: '0xaA61b68301278Da4e001d2cF7F8b2202aed6347c',\n data: '0x00000000000000000000000000000000000000000000000029a2241af62c0000',\n topics: [\n '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',\n '0x0000000000000000000000003b2dbd900e9b23e94270ade010616b9a28293a87',\n '0x000000000000000000000000032f93fae3866af7e86b8022c8ec33b60965de82'\n ],\n index: 21,\n transactionIndex: 4\n }\n]\n\n请注意,查询历史事件可能需要你的提供者支持历史状态查询,这在某些免费提供者中可能是受限的。\n如果你需要查询很早之前的事件,可能需要一个归档节点或者使用专门的区块链索引服务,\n如 The Graph。此外,查询大量的历史数据可能会非常耗时,并且可能会受到节点服务的速率限制。\n在实际应用中,你可能需要考虑分批查询或使用其他策略来优化性能。\n```\n\n\n## 事件监听\n[监听合约事件 ](https://learnblockchain.cn/question/4830)\n```js\nasync function main(){\n\tconst abi = [\n \"event Transfer(address indexed from, address indexed to, uint value)\"\n ]\n\n\tconst address = '0xaA61b68301278Da4e001d2cF7F8b2202aed6347c'\n const contract = new ethers.Contract(address, abi, provider)\n\n\t// Begin listening for any Transfer event\n\tcontract.on(\"Transfer\", (from, to, _amount, event) => {\n\t const amount = ethers.formatEther(_amount)\n\t console.log(666, `${ from } => ${ to }: ${ amount }`);\n\n\t // The `event.log` has the entire EventLog\n\n\t // Optionally, stop listening\n\t event.removeListener()\n\t})\n\n\t/*\n\t// Same as above\n\tcontract.on(contract.filters.Transfer, (from, to, amount, event) => {\n\t // See above\n\t})\n\n\t// Listen for any Transfer to \"ethers.eth\"\n\tfilter = contract.filters.Transfer(\"ethers.eth\")\n\tcontract.on(filter, (from, to, amount, event) => {\n\t // `to` will always be equal to the address of \"ethers.eth\"\n\t})\n\n\t// Listen for any event, whether it is present in the ABI\n\t// or not. Since unknown events can be picked up, the\n\t// parameters are not destructed.\n\tcontract.on(\"*\", (event) => {\n\t // The `event.log` has the entire EventLog\n\t})\n\t*/\n\n}\nmain()\n```",[275,15,16,276,277],"2023-11-20T05:57:06.000Z",{"_id":289,"user_id":8,"username":9,"title":290,"author":9,"category":11,"permlink":291,"body":292,"tags":293,"created":296,"__v":23},"68a4297e9c0586aeab9908f4","ethers.js链上交互指南 / 学习智能合约#65","6yzbyj5d","这些天想升级Dapp的时候却发现web3.js包用不了!web3.js一直以来是又大又难装,这下彻底完犊了,用不了了。这时只能启用备用方案:ethers.js\n\n\n\nhttps://docs.ethers.org/v6/\n\nethers.js更新的挺快,这就6.8的版本啰!\n\n简单对比了下,ethers.js大约是18M, web3.js却有近57M!ethers.js很是小巧灵活。测试了一些写法,感觉还是很丝滑的。而且功能也很全面,爱了!\n\n- [下载与资源](#下载与资源)\n- [安装](#安装)\n- [Gas](#gas)\n- [创建providers](#创建providers)\n- [基本使用](#基本使用)\n- [常用方法](#常用方法)\n- [钱包功能](#钱包功能)\n- [与合约交互](#与合约交互)\n\n## 下载与资源\n[ethers |](https://docs.ethers.org/v6/)\n[ethers-github |](https://github.com/ethers-io/ethers.js)\n[中文文档 |](https://learnblockchain.cn/ethers_v5/) \n\n\n## 安装\n```\nnpm install ethers --save \n// \"ethers\": \"^6.8.0\" 18.3M\n// cnpm install ethers --save \n// yarn add ethers\n```\n\n## Gas\n一经创建,每笔交易都收取一定数量的 gas ,目的是限制执行交易所需要的工作量和为交易支付手续费。EVM 执行交易时,gas 将按特定规则逐渐耗尽。\n\ngas price 是交易发送者设置的一个值,发送者账户需要预付的手续费= gasPrice * gas 。如果交易执行后还有剩余, gas 会原路返还。\n\n## 创建providers\nethers是一套和以太坊节点进行通信的API,是对JSON-RPC的封装。如果我们需要基于以太坊来开发去中心化应用,就需要通过ethers来获取节点状态,获取账号信息,调用合约、监听合约事件等等。\n\n智能合约是运行在节点提供的虚拟机上,因此调用智能合约也需要像节点发送请求。\n```js\nimport { ethers } from 'ethers'\n// const ethers = require('ethers')\n\n//使用本地节点或是远程节点\nconst url = \"http://127.0.0.1:8545\"\n//\"http://localhost:8545\" 会报错\n// const url = \"https://rpc-mumbai.maticvigil.com\"\n// const url = \"https://rpc.ftm.tools/\"\nconst provider = new ethers.JsonRpcProvider(url)\n//交易签名\nconst signer = await provider.getSigner() //默认地址,一般是第一个\n//const signer = await provider.getSigner(\"0x025C4AB61A50Ce391E9F3cd88a858E5C5ADABaF1\") //可以这样指定\n\n//metamask中获取\nprovider = new ethers.BrowserProvider(window.ethereum)\n```\n\n## 基本使用\n```js\n// Look up the current block number\nawait provider.getBlockNumber()\n// 16383845\n\n//获取一个地址或ENS的余额\nlet addr = \"0xf9eFdD67e2C46a8086aed666ea8e294986AB285C\"\nlet balance = await provider.getBalance(addr)\n// 100000000000000000000n\n\n//eth转成wei\nethers.parseEther(\"1.2\")\n//1200000000000000000n\n\n//wei转成 eth\nethers.formatEther('23')\n// 0.000000000000000023\n\n\n//发送交易\nconst option = {\n to:toAccount,\n value: 1500000000, //wei\n gas: gas,\n gasPrice: gasPrice\n}\neg:\nconst tx = await signer.sendTransaction({\n to: \"0xf33B812142b8a008cD98ED889605869B31393849\",\n value: ethers.parseEther(\"1.0\")\n })\nconst receipt = await tx.wait()\nconsole.log(899, receipt) \n/*\nTransactionReceipt {\n provider: JsonRpcProvider {},\n to: '0xf33B812142b8a008cD98ED889605869B31393849',\n from: '0xf9eFdD67e2C46a8086aed666ea8e294986AB285C',\n contractAddress: null,\n hash: '0xafac68a6a871424bf2aa4da66b7aed38c379bd335e56f8bfecdea680febac5ed',\n index: 0,\n blockHash: '0xb5e876eae0264cdfbbb7e8d0e3676ed31abc6121d78fc9080498fea3bdafa3d2',\n blockNumber: 1,\n logsBloom: '0x0000000....00',\n gasUsed: 21000n,\n cumulativeGasUsed: 21000n,\n gasPrice: 20000000000n,\n type: 0,\n status: 1,\n root: undefined\n}\n*/\n```\n\n## 常用方法\n```js\n1. 单位换算\n//eth转成wei\nethers.parseEther(\"1.2\")\n//1200000000000000000n\n//wei转成 eth\nethers.formatEther('23')\n// 0.000000000000000023\n\n//另外一种转换方法\nethers.parseUnits(\"1.3\", 18) // 乘以10**18\n//1300000000000000000n\nethers.formatUnits(\"1300000000000000000\", 18) // 除以10**18\n//1.3\n\n2. gasLimit \nsigner.estimateGas(tx)\n通过执行一个消息调用来得到交易的 gas 用量, 在此基础上加30000gas\n\n3. GasPrice\nawait provider.getFeeData() //获取当前gas价格\n/*\nFeeData {\n gasPrice: 20000000000n,\n maxFeePerGas: null,\n maxPriorityFeePerGas: null\n}*/\n\n4. 查找最新区块号\nawait provider.getBlockNumber()\n//2655567\n```\n\n## 钱包功能\n[手册](https://learnblockchain.cn/docs/ethers.js/)\n[新帐户](https://learnblockchain.cn/2018/10/25/eth-web-wallet_1)\n```js\nimport { ethers } from 'ethers'\nimport fs from 'fs'\n\n//直接生成私钥和地址\nlet lbcWallet = ethers.Wallet.createRandom()\nlbcWallet.address\n// \"0x863e27dbD608649d08c69F83ccA51b045721f318\"\nlbcWallet.privateKey\n// \"0x8bc29xxxxxxx\"\n\n//随机数生成钱包\nlet random = ethers.randomBytes(32) \nconsole.log(563, random)\nlet privateKey = Buffer.from(random).toString('hex') //转成16进制\nconsole.log(112, privateKey) \nlet wallet = new ethers.Wallet(privateKey)\nconsole.log(\"账号地址: \" + wallet.address, wallet)\n\n//生成随机助记词并创建钱包\nlet mnemonic = ethers.Mnemonic.fromEntropy(ethers.randomBytes(16))\n// let mnemonic = ethers.Mnemonic.fromEntropy(ethers.randomBytes(16), \"jxxxx\") 第二个参数是密码\nconsole.log(1323, mnemonic)\nvar path = \"m/44'/60'/0'/0/0\"\n// 通过助记词创建钱包\nlet wallet = ethers.HDNodeWallet.fromMnemonic(mnemonic, path) \nconsole.log(\"账号地址: \" + wallet.address, wallet)\n\n//根据助记词找回钱包信息\nlet monic = \"dinosaur tape orbit chronic private .....\"\nlet mnemonic = ethers.Wallet.fromPhrase(monic)\nconsole.log(566, mnemonic)\nlet privateKey = mnemonic.privateKey\nconsole.log(\"钱包私钥:\",privateKey)\n\n//根据私钥找回钱包地址\nlet wallet = new ethers.Wallet(privateKey)\n//钱包地址\nlet address = wallet.address\nconsole.log(156, address)\n\n//生成json钱包文件\nlet main = async function (){\n let random = ethers.randomBytes(32) \n let privateKey = Buffer.from(random).toString('hex') //转成16进制\n let wallet = new ethers.Wallet(privateKey)\n let password = \"xxxxx\"\n let res = await wallet.encrypt(password)\n console.log(156, res)\n let d = new Date()\n let time = d.getTime()\n let path = './keystore/' + time + '-' + wallet.address\n fs.writeFileSync(path, res)\n}\n\n//json文件找回钱包信息\nlet main2 = async function (){\n let res = fs.readFileSync('./keystore/16983xxxxxxxxxxxB2c6', \"utf8\")\n let password = \"xxxxxx\"\n let wallet = await ethers.Wallet.fromEncryptedJson(res, password)\n console.log(\"Address: \" + wallet.address, wallet,\"privateKey:\", wallet.privateKey)\n}\n```\n\n## 与合约交互\n```js\n//只读方法 (like view and pure)\nimport { ethers } from 'ethers'\nconst url = \"http://127.0.0.1:8545\"\nconst provider = new ethers.JsonRpcProvider(url)\nlet abi = [\n \"function str() view returns (string)\",\n \"function set(uint x)\",\n \"function get()view returns (uint)\"\n ]\n//这里的abi相当于接口\n// 也可以使用合约的编译abi: import Storage from '@/static/Storage.json' -> Storage.abi\nlet contractAddr = \"0x600a00aE84b896edd9F2732565cf2195d032315E\"\nlet contract = new ethers.Contract(contractAddr, abi, provider)\nlet str = await contract.get()\nconsole.log(256, str)\n\n//合约的所有方法, signer签名\nconst url = \"http://127.0.0.1:8545\"\nconst contractAddr = \"0x600a00aE84b896edd9F2732565cf2195d032315E\"\nconst provider = new ethers.JsonRpcProvider(url)\nconst signer = await provider.getSigner()\nconst contract = new ethers.Contract( contractAddr, Storage.abi, signer)\nconsole.log(56, \"signer:\", signer)\nconst option = { \n}\nconst tx = await contract.set(996, option)\nawait tx.wait()\nconsole.log(669, \"singen res:\", tx)\n\n//合约的所有方法, 也可以连上钱包来实现\nimport Storage from '@/static/Storage.json'\nconst url = \"http://127.0.0.1:8545\"\nconst contractAddr = \"0x600a00aE84b896edd9F2732565cf2195d032315E\"\nconst provider = new ethers.JsonRpcProvider(url)\nconst contract = new ethers.Contract(contractAddr, Storage.abi, provider)\nconst PRIVATE_KEY = \"0x182xxxxxxxxx\"\nconst wallet = new ethers.Wallet(PRIVATE_KEY, provider)\nconsole.log(56, \"wallet:\", wallet)\nconst nonce = await provider.getTransactionCount(wallet.address)\nconsole.log(58, \"nonce:\", nonce)\nconst StorageConnected = contract.connect(wallet)\nconst option = {\n nonce\n}\nconst tx = await StorageConnected.set(86394, option)\nawait tx.wait()\nconsole.log(669, \"res:\", tx)\n```\n\nethers的体验相当不错,功能齐全,它的转正也是情理之中啰。\n",[275,15,276,277,294,295],"contract","blockchian","2023-10-26T03:41:30.000Z",{"_id":298,"user_id":8,"username":9,"title":299,"author":9,"category":11,"permlink":300,"body":301,"tags":302,"created":305,"__v":23},"68a4297e9c0586aeab990804","merkleTree验证白名单 / 学习智能合约#64","5nyopa1d","最近FTX爆了,据称是挪用了用户资金!不管是什么技术,最后还是回到了灵魂拷问:利和义,你取哪样?!吃瓜群众们又在那想验证资金的办法,怎么自证清白之类的。其实我想说,这只关乎人性,无关技术。如果真的关乎技术,那应该有N种办法。\n\n这里提这N种办法中的一种:merkletree证明。现在常用于白名单验证:比如证明我是否在名单里呢?电影中辛德勒有个小本本会把名单记下来,很容易查到谁在他的名单里。我们也要把名单记下来,然后一个个去查么?如果人少,这样做没问题。但人一多,特别是gas费很贵的情况下,要在链上维护你的小本本,不是个好办法。最佳的办法是用merkletree证明。\n\n\n\n如上图所示,我们可以把名单做成一个树状的结构,两两一组,逐级上联,最后形成一个根。我们只需把最后的根记在链上即可。这有点类似于:我们只需要字典的检索部分即可,真正的内容可以通过检索去查。那么,这里汲及到merkletree是如何证明一个数据是真实在名单里的,其实只要用到哈希函数。\n```js\nconst { MerkleTree } = require('merkletreejs')\nconst keccak256 = require('keccak256')\n\nlet whitelist = [\n \"0xc11cb63bdca7627f74ec69c14612522fdb7a0c20\",\n \"0x1499b8312e6fe58b5d1164d4eccf795367c9e1d3\",\n \"0x55f510be6ab4c7e07ec6ee637aa83574975d6898\",\n \"0xcc2fe3615a45fcacc3534d53be41c6543a0a312d\",\n \"0xee226379db83cffc681495730c11fdde79ba4c0c\",\n \"0x55f510be6ab4c7e07ec6ee637aa83574975d6898\",\n \"0x18b2a687610328590bc8f2e5fedde3b582a49cda\",\n \"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\"\n]\n\nconst leafNodes = whitelist.map(addr => keccak256(addr));\nconst merkletree = new MerkleTree(leafNodes, keccak256, {sortPairs: true});\nconsole.log(12, \"leafNodes\",leafNodes)\n\nconst rootHash = merkletree.getRoot().toString('hex');\nconsole.log(\"rootHash is: \", rootHash);\nconsole.log(merkletree.toString());\n\nconsole.log(\"--------verify------------\");\nconst hexProof = merkletree.getHexProof(keccak256(\"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\")); \nconsole.log(133, hexProof); // 如上图所示中的数据[B, C, D, E]\nconst res = merkletree.verify(hexProof, keccak256(\"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\"), rootHash) \nconsole.log(124,res); \n```\n\n以上给出了构建merkletree的方法和验证的过程,细心体会下,貌似也不太难。回到链上去证明,过程类似,方法略有区别,这里也展示下:\n\n```js\nfunction verify(\n bytes32[] memory proof,\n bytes32 root,\n bytes32 leaf\n) internal pure returns (bool) {\n return processProof(proof, leaf) == root;\n}\n\nfunction whitelist(bytes32[] calldata _merkleProof) public{\n bytes32 leaf = keccak256(abi.encodePacked(msg.sender));\n require(verify(_merkleProof, merkleRoot, leaf), \"Invaild proof.\");\n}\n```\n\nsolidity的实现还算简单。这样在链上只需维护merkletree的根哈希(merkleRoot),链下则维护真正的名单,只是这个维护的办法不是辛德勒的小本本而是merkletree的结构。\n\n",[275,15,276,31,303,304,279],"truffle","merkletree","2022-11-09T06:24:39.000Z",{"_id":307,"user_id":8,"username":9,"title":308,"author":9,"category":11,"permlink":309,"body":310,"tags":311,"created":313,"__v":23},"68a4297e9c0586aeab990802","节约gas小妙招 / 学习智能合约#63","dk8vit5g","把节约gas的几个方法测试了下,蛮有效的。以前看起来有些多余的操作其实是另有深意啊。比如常见的语句`uint _arr = arr[i]`,直接写`arr[i]`不是更直观么,为什么非要定义一个变量呢?!现在终于揭晓了谜团:原来是为了节约gas啊!\n\n[案例集](https://solidity-by-example.org/gas-golf/) 中把这些方法都总结了,我也测试了下,还是很实用嘀!\n\n```js\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n// gas golf\ncontract GasGolf {\n // start 56530 gas \n //public -> external 最小化权限 56530 gas\n //变量memory -> calldata 54104 gas\n //多个条件变短 53816 gas\n // total -> _total 加载状态变量到内存变量 52291 gas \n // nums.length -> len 数组长度缓存到内存 52232 gas\n // nums[i] -> num 缓存多次使用的数组元素到内存 51581 gas\n //i+=1 -> ++i 51023 gas\n\n uint public total;\n\n // start - not gas optimized [2, 10, 30, 55, 56, 62, 52, 2, 16]\n function sum1(uint[] memory nums) external {\n for (uint i = 0; i \u003C nums.length; i += 1) {\n bool isEven = nums[i] % 2 == 0;\n bool isLessThan99 = nums[i] \u003C 99;\n if (isEven && isLessThan99) {\n total += nums[i];\n }\n }\n }\n\n // gas optimized\n function sum2(uint[] calldata nums) external {\n uint _total = total;\n uint len = nums.length;\n\n for (uint i = 0; i \u003C len; ) {\n uint num = nums[i];\n if (num % 2 == 0 && num \u003C 99) {\n _total += num;\n }\n unchecked {\n ++i;\n }\n }\n\n total = _total;\n }\n}\n```\n\n测试合约如上,大家也可以尝试着去做。**节约gas的方法可以总结如下**:\n1. 最小化权限控制,尽量用`external, private, internal, 尽量不用public`\n2. 变量类型优先使用`calldata`而不是`memory`\n3. 加载状态变量到内存变量\n4. 多个条件判断使用短路方式\n5. 在循环中使用 `++i,而不是i+=1, i++`\n6. 数组长度缓存到内存\n7. 缓存多次使用的数组元素到内存",[275,15,276,31,303,312,279],"gas","2022-11-07T19:09:09.000Z",{"_id":315,"user_id":8,"username":9,"title":316,"author":9,"category":11,"permlink":317,"body":318,"tags":319,"created":322,"__v":23},"68a4297e9c0586aeab990800","multicall一次获取多条数据 / 学习智能合约#62","1h0rxhs9","智能合约中的数据获取一般都有点难度,不是速度慢,就是节点限速等。对于要求速度快且准确的应用来讲,如何快速地获取数据就是一个值得重视的问题。\n\n对于我来讲,我也遇到了相似的问题:在NFT合约中如何快速读取全部或是多数的NFT数据。大家都知道,一个NFT合约都是几百上千,很多是一万个数量,如何一次性读取多个,也尝试过不少方法。当然,现在推荐的方法是multicall。\n\nmulticall可以一次性读取一个或多个合约的函数,快速得到结果。它主要是通到`call` 或 `staticcall`来读取合约数据。这两个函数比较底层,理解起来有些难度,而且还要提前计算出`calldata`。\n\n```js\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract MultiCall {\n function multiCall(address[] calldata targets, bytes[] calldata data)\n external\n view\n returns (bytes[] memory)\n {\n require(targets.length == data.length, \"target length != data length\");\n\n bytes[] memory results = new bytes[](data.length);\n\n for (uint i; i \u003C targets.length; i++) {\n (bool success, bytes memory result) = targets[i].staticcall(data[i]);\n require(success, \"call failed\");\n results[i] = result;\n }\n\n return results;\n }\n}\n\n//测试合约,最下面两个是计算 `calldata`的方法\ncontract Test {\n function test1() external view returns (uint,uint) {\n return (1, block.timestamp);\n }\n\n function test2() external view returns (uint,uint) {\n return (2, block.timestamp);\n }\n\n function getData1() external pure returns (bytes memory) {\n return abi.encodeWithSelector(this.test1.selector); //计算出`calldata`\n }\n\n function getData3() external pure returns (bytes memory) {\n return abi.encodeWithSignature(\"test2()\"); //计算出`calldata`\n }\n}\n```\n\nmulticall合约只有一个方法,用合约地址和calldata去调用函数获取结果,一般只应用于view属性的函数。合约地址就明白,`calldata`要如何去计算呢?如上所示的`Test`合约中`getData`列出两种计算`calldata`的方法,略有差异,但结果一致。在应用较复杂,需要一次性得到多条数据的情况下,multicall就非常适用啰!\n",[275,15,276,31,320,321,279],"multicall","calldata","2022-11-02T06:42:45.000Z",{"_id":324,"user_id":8,"username":9,"title":325,"author":9,"category":11,"permlink":326,"body":327,"tags":328,"created":332,"__v":23},"68a4297e9c0586aeab9907fc","代理升级模式 / 学习智能合约#61","tnza9jgp","前不久研究了下重入攻击的玩法,里面有个重要的特性:**合约的fallback()函数。** fallback是合约的底层函数,在没有其它函数匹配的情况下会执行。这个特性导致了重入的风险性,但在另一方面,合约的升级性也会用到这一特性。这充分体现了一物的两面性:是好是坏取决于你!\n\n由于区块链不可篡改性,合约如果有漏洞就很难更改!那么,怎么才能安全地升级合约呢?这里要用到一种解藕的想法。把一个合约分成:代理、数据和逻辑三部分,通常要升级的也就是逻辑部分,其它不受影响,如下图所示:\n\n\n\n我们来看下具体实现:\n\n```js\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n//定义数据合约\ncontract storageStructure {\n //记录球员和分数\n address public implementation;//逻辑合约地址\n mapping(address=>uint256) public points;\n address public owner;\n}\n\n//定义逻辑合约\ncontract implementationV1 is storageStructure {\n modifier onlyowner() {\n require(msg.sender == owner, \"only owner can do\");\n _;\n }\n //增加球员和分数\n function addPlayer(address player, uint256 point) public onlyowner {\n require(points[player] == 0, \"player already exists\");\n points[player] = point;\n }\n //修改球员和分数\n function setPlayer(address player, uint256 point) public onlyowner {\n require(points[player] != 0, \"player must already exists\");\n points[player] = point;\n }\n}\n\n//代理合约 代理合约调用逻辑合约的逻辑去修改本身(代理合约)的数据\ncontract proxy is storageStructure {\n modifier onlyowner() {\n require(msg.sender == owner, \"only owner can call\");\n _;\n }\n \n constructor() {\n owner = msg.sender;\n }\n \n //更新逻辑合约的地址\n function setImpl(address _impl) public onlyowner {\n implementation = _impl;\n }\n \n //fallback函数 调用逻辑合约中的函数,在本地(代理合约)执行\n fallback() external {\n address impl = implementation; //逻辑合约的地址\n require(impl != address(0), \"implementation must exists\");\n \n //底层调用\n assembly {\n //调用delegateccall\n let ptr := mload(0x40)\n calldatacopy(ptr, 0, calldatasize())\n //delegatecall(g, a, in, insize, out, outsize)\n let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0)\n let size := returndatasize()\n \n //returndatacopy(t, f, s)\n returndatacopy(ptr, 0, size)\n \n switch result \n case 0 { revert(ptr, size) }\n default { return(ptr, size) }\n }\n } \n}\n```\n\n可以看出,里面最核心的就是`assembly中调用delegateccall`。`assembly`属于汇编的写法,是比较底层的语法(yul),不太好理解。我们大概可以理解整个地调用过程:用户调用代理合约,代理合约调用逻辑合约修改本身的数据。**如果需要升级,代理合约调用逻辑合约时就可以指向新的逻辑合约即可!**\n\n\n\n```js\n//逻辑合约升级\ncontract implementationV2 is implementationV1 {\n function addPlayer(address player, uint256 point) override public onlyowner virtual {\n require(points[player] == 0, \"player already exists\");\n points[player] = point;\n totalPlayers ++;\n }\n}\n```\n\n升级合约差不多就是个解藕的思路,分成三个部分,把需要升级的部分单独更改升级就可以啰!",[275,15,276,329,330,331,279],"implementation","assembly","proxy","2022-10-30T01:46:27.000Z",{"_id":334,"user_id":8,"username":9,"title":335,"author":9,"category":11,"permlink":336,"body":337,"tags":338,"created":341,"__v":23},"68a4297e9c0586aeab9907fa","call和delegatecall合约调用 / 学习智能合约#60","vcfm6xl1","昨天研究了下重入攻击,有查到`call`这个函数,感觉它很万能--几乎能干所有的事情!既能转帐以太,又能调用合约,也就无怪乎有重入风险。现在solidity早就建议用`transfer`来转帐以太,调用合约用接口(interface)。其实用call来调用合约也是很方便的,同时还有个类似的函数`delegatecall`。\n\n调用另一个合约中的函数,主要是两个方法:call 和 delegatecall。但推荐用接口调用!\ncall 会切换到被调合约中去执行方法,会切换上下文。msg.sender是主调合约地址。\ndelegatecall 则是将被调合约中的方法调到本合约中执行,也就是不会切换上下文。msg.sender是发起者本人。\n\n传入的参数(address _addr)是合约地址。下边有个案例,可以试着去体会细节。\n```js\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract SetNum {\n uint256 public n = 20;\n address public sender;\n\n function setN(uint256 _n) public {\n n = _n;\n sender = msg.sender;\n }\n}\n\ncontract CallSet {\n uint256 public n = 15;\n address public sender;\n\n function CallTest(address _addr, uint256 _n) public {\n _addr.call(abi.encodeWithSignature(\"setN(uint256)\", _n)); //改变SetNum中的n值\n sender = msg.sender; //是调用者本人,同时会改变SetNum的sender地址\n }\n\n function delegatecallTest(address _addr, uint256 _n) public {\n _addr.delegatecall(abi.encodeWithSignature(\"setN(uint256)\", _n)); //改变自身的n值\n sender = msg.sender; //是调用者本人,不会改变SetNum的sender地址\n }\n}\n```\n\ncall和delegatecal这两个方法做了很大改进,我估计也是为了防止重入的风险。以前是要算出函数的ID值,现在是计算整个的签名值。刚刚还不知错误在哪,到google上又是一顿好找,终于解决!solidity的文件写得干巴巴的,不好理解。找了个[案例集](https://solidity-by-example.org),很不错,分享下。\n",[275,15,276,279,339,340],"call","delegatecall","2022-10-27T05:13:12.000Z",[343,351,360,367,374,381,388,396,403,410,417,424,431,438,445,452,459,466,473,480],{"_id":344,"user_id":8,"username":9,"title":345,"author":9,"category":11,"permlink":346,"body":347,"tags":348,"created":350,"__v":23},"68a4297e9c0586aeab9907a0","宝剑劈柴歌","nswhfcy9","十年风霜磨一剑,嗜血锋芒隐欲现。\n\n可怜世无屠龙术,直教劈柴做火钳!",[15,16,349],"songofera","2022-06-30T19:19:30.000Z",{"_id":352,"user_id":8,"username":9,"title":353,"author":9,"category":11,"permlink":354,"body":355,"tags":356,"created":359,"__v":23},"68a4297d9c0586aeab9906ea","凝固的时光 --时代的歌之282","282","收藏起冬衣,\n放一颗樟脑丸,\n在这香气凝固的时光里,\n期待一个崭新的冬天。",[15,16,349,357,358],"poetry","cn-book","2021-06-07T05:19:21.000Z",{"_id":361,"user_id":8,"username":9,"title":362,"author":9,"category":11,"permlink":363,"body":364,"tags":365,"created":366,"__v":23},"68a4297d9c0586aeab9906b8","碎光阴--时代的歌之281","281","常道革命不新鲜,万币齐发各争妍。挖矿开发又一天。\n各路神仙争索道,手段齐出只围观。常奉老庄不增添。",[15,16,349,357,358],"2021-04-24T02:58:03.000Z",{"_id":368,"user_id":8,"username":9,"title":369,"author":9,"category":11,"permlink":370,"body":371,"tags":372,"created":373,"__v":23},"68a4297d9c0586aeab9905e0","月影 --时代的歌之280","skfbq","\n\n过去之时已成蜕,未来之势谜影雾。\n乘风破浪未有时,山阻冰塞当前路。\n\n孜孜以求暗夜短,个中滋味谁品读?\n风起月涌银光洒,一片初心仍从容。\n",[15,16,349,357,358],"2020-09-22T17:07:57.000Z",{"_id":375,"user_id":8,"username":9,"title":376,"author":9,"category":11,"permlink":377,"body":378,"tags":379,"created":380,"__v":23},"68a4297d9c0586aeab990504","诗意的酒 --时代的歌之279","wgzxp","\n\n今夜,长空如此美丽!\n微风送爽,\n月辉如水,\n静谧如画,\n微醺-------\n仿佛置身仙境般的月宫!\n\n透过这如水晶般的蓝色天幕,\n似有充满快乐的蜃楼天国,\n没有新冠肆虐,\n世人快活。\n\n在夜光中漂流,\n奔向我理想的王国!\n\n",[15,16,349,357,358],"2020-05-01T03:38:27.000Z",{"_id":382,"user_id":8,"username":9,"title":383,"author":9,"category":11,"permlink":384,"body":385,"tags":386,"created":387,"__v":23},"68a4297d9c0586aeab9904d8","你要把整个世界吃入肚肠… --时代的歌之278","6o4yx","你要把整个世界吃入肚肠,\n你这吃人的恶魔,\n无聊的小丑!\n\n你磨尖你的利牙,\n却戴上伪善的面孔,\n张大灯笼似的目光,\n把世界攫取!\n\n你善于操弄人心,\n更甚于你的利爪。\n将自私发挥到顶端,\n描绘着空无的虚幻,\n影从着愚蠢盲目的人群,\n却不知前面是你无底的黑洞!\n\n哦,你要把整个世界吃入肚肠,\n赢得一片掌声,\n还有一声鄙夷地唾弃!\n\n\n\n",[15,16,349,357,358],"2020-04-05T16:37:21.000Z",{"_id":389,"user_id":8,"username":9,"title":390,"author":9,"category":15,"permlink":391,"body":392,"tags":393,"created":395,"__v":23},"68a4297d9c0586aeab990384","减法--时代的歌之277","277","每过一天,\n日子就减少一天。\n当减到零时,\n日子也就过完了。\n\n\n@lemooljiang #songofera\n\n\n\n",[15,16,349,394,358],"life","2019-05-29T05:41:48.000Z",{"_id":397,"user_id":8,"username":9,"title":398,"author":9,"category":15,"permlink":399,"body":400,"tags":401,"created":402,"__v":23},"68a4297d9c0586aeab99037e","胡椒粉和剃须刀--时代的歌之276","276","胡椒粉和剃须刀,\n这两者完成不搭界,\n也没有任何联系。\n唯一能把它们联结在一起的是——\n它们俩在我的购物车里紧靠着!\n而我——\n却试图进行一场神秘主义的猜想!\n\n@lemooljiang #songofera \n\nhttps://cdn.steemitimages.com/DQmRgTRVP4ffYnBC51juVQAJhrQKVzwqF4Zr6KiRBU5gRHp/mysterious.jpg",[15,16,349,394,358],"2019-05-24T06:54:36.000Z",{"_id":404,"user_id":8,"username":9,"title":405,"author":9,"category":15,"permlink":406,"body":407,"tags":408,"created":409,"__v":23},"68a4297d9c0586aeab99037a","修路--时代的歌之275","275","\n\n\n\n好好一条路,\n天天修又拆。\n行人不得进,\n路人不得通。\n\n为何年年修?\n天天打桩声?\n扰人清梦多,\n出行俱不便。\n\n政绩来得快,\n财税好报销。\n试问行路人,\n路况改良未?\n\n\n\n\n@lemooljiang #songofera",[15,16,349,394,358],"2019-05-22T03:42:36.000Z",{"_id":411,"user_id":8,"username":9,"title":412,"author":9,"category":15,"permlink":413,"body":414,"tags":415,"created":416,"__v":23},"68a4297d9c0586aeab990306","如梦令--时代的歌之274","274","\n\n\n\n呆坐书堂月影,\n单衣不敌春寒。\n全日放飞自我,\n刷卡才知艰难。\n哈弃,哈弃,\n牛肉火锅走起。\n\n\n@lemooljiang #songofera",[15,16,349,394,358],"2019-02-26T07:01:06.000Z",{"_id":418,"user_id":8,"username":9,"title":419,"author":9,"category":15,"permlink":420,"body":421,"tags":422,"created":423,"__v":23},"68a4297d9c0586aeab9902fe","活着--时代的歌之273","273","\n\n\n\n活着不易,\n当所有苦难和不幸都降于一人之身,\n只能逆来顺受,挣扎求存。\n活着就有一道微光,\n再重的黑夜也将褪去。\n\n活着的痛苦更甚于死亡的痛苦,\n苦难的意义更甚于幸福的意义。\n人类的历史就是一部苦难史,\n苦难之花使生命变得鲜活。\n\n@lemooljiang #songofera",[15,16,349,394,358],"2019-02-19T05:00:45.000Z",{"_id":425,"user_id":8,"username":9,"title":426,"author":9,"category":15,"permlink":427,"body":428,"tags":429,"created":430,"__v":23},"68a4297d9c0586aeab9902fc","家乡的腊肉--时代的歌之272","272","\n\n\n临行前的殷殷嘱托,\n家乡的腊肉几乎已成必备。\n箱子再大,\n也装不下老妈唠叨地关怀。\n\n在阳光充沛的冬日下午,\n一条条腊肉饱吸着这乡土气息,\n肥瘦均匀、风味独特,\n这是来自家乡的特产。\n\n无论你身在何处、相隔多久,\n这是一段存储在舌尖上的深刻记忆。\n\n\n\n@lemooljiang #songofera",[15,16,349,394,358],"2019-02-17T04:45:45.000Z",{"_id":432,"user_id":8,"username":9,"title":433,"author":9,"category":15,"permlink":434,"body":435,"tags":436,"created":437,"__v":23},"68a4297d9c0586aeab9902fa","春运的片刻--时代的歌之271","271","\n\n\n\n30亿人次的忧思渴盼,\n谁能载得动?!\n\n积攒了一年的情债,\n你迟早得清还。\n哦,时光的无情沙漏,\n最终也将落入温情的海滩。\n\n归家的路途是一场硬仗,\n再勇猛的战士也会灰头土脸,\n鼻青脸肿。\n把我从地狱的锅中蒸出,\n不改初心!\n\n不能忘记缱绻的慰问,\n虽然我默不出声。\n\n@lemooljiang #songofera",[15,16,349,394,358],"2019-01-28T20:50:06.000Z",{"_id":439,"user_id":8,"username":9,"title":440,"author":9,"category":15,"permlink":441,"body":442,"tags":443,"created":444,"__v":23},"68a4297d9c0586aeab9902dc","即景 --时代的歌之270","270","https://776f-woshiwang-c04120-1258142086.tcb.qcloud.la/steemit/boating.jpg\n\n\n小船摇荡我心波,\n望着你时心咚嗦。\n想说爱你不容易,\n摇船揺到手脱力。\n\n@lemooljiang #songofera",[15,16,349,394,358],"2019-01-07T06:08:09.000Z",{"_id":446,"user_id":8,"username":9,"title":447,"author":9,"category":15,"permlink":448,"body":449,"tags":450,"created":451,"__v":23},"68a4297d9c0586aeab9902d2","意难伸 --时代的歌之269","269","https://i.ibb.co/Xkbt73d/feel.jpg\n\n\n\n少年子弟江湖老,\n倾国倾城瓦砾堆。\n斗转星移时光少,\n壮志难酬意难伸。\n\n@lemooljiang #songofera",[15,16,349,394,358],"2018-12-29T04:37:27.000Z",{"_id":453,"user_id":8,"username":9,"title":454,"author":9,"category":15,"permlink":455,"body":456,"tags":457,"created":458,"__v":23},"68a4297d9c0586aeab9902c6","寒冬--时代的歌之268","268","\n\n\n\n天寒白酒烈,\n地冻火锅鲜。\n币多灾情显,\n明年见不见?\n\n\n@lemooljiang #songofera",[15,16,349,394,358],"2018-12-15T05:04:18.000Z",{"_id":460,"user_id":8,"username":9,"title":461,"author":9,"category":15,"permlink":462,"body":463,"tags":464,"created":465,"__v":23},"68a4297d9c0586aeab9902ac","手机--时代的歌之267","267","\n\n\n掌中小世界,\n内里大乾坤。\n吃喝玩乐颓,\n众生浮世绘。\n\n\n@lemooljiang #songofera",[15,16,349,394,358],"2018-11-23T05:14:27.000Z",{"_id":467,"user_id":8,"username":9,"title":468,"author":9,"category":15,"permlink":469,"body":470,"tags":471,"created":472,"__v":23},"68a4297d9c0586aeab990270","我会去爬帽峰山--时代的歌之266","266","\n\n在我开心的时候,我会去爬帽峰山…\n在我伤心的时候,我会去爬帽峰山…\n在有太阳的时候,我会去爬帽峰山…\n在下起雨的时候,我会去爬帽峰山…\n在有好友相伴的时候,我会去爬帽峰山…\n在我孤独的时候,我会去爬帽峰山…\n在我腰缠万贯的时候,我会去爬帽峰山…\n在我不名一文的时候,我会去爬帽峰山…\n在我工作忙的时候,我会去爬帽峰山…\n在我空闲的时候,我会去爬帽峰山…\n在我年经的时候,我会去爬帽峰山…\n在我老了的时候,我会去爬帽峰山…\n在现在的时候,我会去爬帽峰山…\n在未来的时候,我会去爬帽峰山…\n在现实中的时候,我会去爬帽峰山…\n在梦里的时候,我会去爬帽峰山…\n\n我,一个会去爬帽峰山的\n只干一件事的\n——无聊之人!\n\n\n@lemooljiang #songofera",[15,16,349,394,358],"2018-10-03T04:21:00.000Z",{"_id":474,"user_id":8,"username":9,"title":475,"author":9,"category":15,"permlink":476,"body":477,"tags":478,"created":479,"__v":23},"68a4297d9c0586aeab990266","头昏昏--时代的歌之265","265","\n\n\n指手画脚哗啦,\n语气昂扬吧差,\n头昏眼花飞鸦,\n找把椅子靠着,\n--------睡啦!\n\n一分一秒嘀哒,\n空虚茬子满啦,\n幸好冷气哗哗,\n挨到散会呱呱,\n鼓完掌就散啦!\n\n@lemooljiang #songofera",[15,16,349,394,358],"2018-09-21T02:44:33.000Z",{"_id":481,"user_id":8,"username":9,"title":482,"author":9,"category":15,"permlink":483,"body":484,"tags":485,"created":486,"__v":23},"68a4297d9c0586aeab990260","所知的未知--时代的歌之264","264","\n\n\n\n在黑暗中睁开眼,\n没有一丝光亮。\n放空大脑,做些奇怪地动作,\n竟有点冥想悠然的味道。\n\n命运之河有几多分叉,\n未知又在何处聚集?\n人生——\n不过是盲人摸象!\n\n\n@lemooljiang #songofera",[15,16,349,394,358],"2018-09-12T03:42:48.000Z",["Reactive",488],{},["Set"],["ShallowReactive",491],{"$fRCMMtu5QPcogUAQq-b-1ppHnEgYqllcSGLeeBmHjnjo":-1},true,"/lemooljiang"]