heyuan 发布的文章 - 六币之门
首页
视频教程
网站导航
活动日历
关于我们
用户投稿
推荐
新闻动态
搜 索
1
融资周报 | 公开融资事件11起;加密技术公司Toposware完成500万美元融资,Polygon联创参投
112 阅读
2
六币日报 | 九只比特币ETF在6天内积累了9.5万枚BTC;贝莱德决定停止推出XRP现货ETF计划
76 阅读
3
融资周报 | 公开融资事件27起;L1区块链Monad Labs完成2.25亿美元融资,Paradigm领投
74 阅读
4
六币日报 | 美国SEC再次推迟对灰度以太坊期货ETF做出决定;Do Kwon已出黑山监狱等待引渡
72 阅读
5
【ETH钱包开发06】查询某个地址的交易记录
55 阅读
新闻动态
每日快报
一周精选
融资情况
项目投研
自治组织
数字藏品
去中心化应用
去中心化游戏
去中心化社交
去中心化金融
区块链交易所
科普知识
小白入门
用户手册
开发文档
行业报告
技术前沿
登录
搜 索
标签搜索
新闻
日报
元歌Eden
累计撰写
1,087
篇文章
累计收到
0
条评论
首页
栏目
新闻动态
每日快报
一周精选
融资情况
项目投研
自治组织
数字藏品
去中心化应用
去中心化游戏
去中心化社交
去中心化金融
区块链交易所
科普知识
小白入门
用户手册
开发文档
行业报告
技术前沿
页面
视频教程
网站导航
活动日历
关于我们
用户投稿
推荐
新闻动态
用户登录
登录
找到
1087
篇与
heyuan
相关的结果
2023-03-24
java通过web3j开发以太坊过滤器filter和事件event
web3j过滤器提供以太坊网络发生的某些事件的通知,对java和安卓程序员来说很有用。在Ethereum以太坊中支持三类过滤器:块滤波器(Block filters) 未决交易过滤器(Pending transaction filters) 主题过滤器(Topic filters) 块过滤器和未决交易过滤器提供了在网络上创建新交易或块的通知。主题过滤器更灵活。允许根据提供的特定标准创建过滤器。不幸的是,除非你使用WebSocket连接到Geth,否则通过JSON-RPC API来处理过滤器是一个繁琐的过程,这里需要轮询以太坊客户端,以便了解HTTP和IPC所请求的实时同步特征,是否有任何新的更新到你的过滤器。此外,块和交易过滤器只提供交易或区块链hash值,因此需要进一步的请求来获得hash对应的实际交易或块。web3j的过滤器解决了这些问题,因此你有一个完全异步的基于事件的API来处理过滤器。它使用RXJava的可观测性Observables,它提供了与事件协同工作的一致API,这有助于通过功能组合将JSON-RPC调用链接在一起。注:Infura不支持过滤器。块和交易过滤器接收所有新块把它们添加到区块链(false参数指定我们只需要块就ok,而不需要嵌入交易):Subscription subscription = web3j.blockObservable(false).subscribe(block -> { ... }); 复制代码接收所有新交易,把它们添加到块链:Subscription subscription = web3j.transactionObservable().subscribe(tx -> { ... }); 复制代码接收所有待提交交易并提交到网络(即在它们被分组在一起之前):Subscription subscription = web3j.pendingTransactionObservable().subscribe(tx -> { ... }); 复制代码不再需要的时候取消订阅unsubscribe:subscription.unsubscribe(); 复制代码另外还提供了其他回调,它们简单地提供了块或交易hash,这些细节涉及Web3JRX接口。再现过滤器webjs还提供用于再现块和交易历史的过滤器。从区块链再现一系列块:Subscription subscription = web3j.replayBlocksObservable( <startBlockNumber>, <endBlockNumber>, <fullTxObjects>) .subscribe(block -> { ... }); 复制代码再现包含在一个块范围内的单个交易:Subscription subscription = web3j.replayTransactionsObservable( <startBlockNumber>, <endBlockNumber>) .subscribe(tx -> { ... }); 复制代码也可以获得Web3J再现最新的块,并在你看过后提供通知(通过提交Observable):Subscription subscription = web3j.catchUpToLatestBlockObservable( <startBlockNumber>, <fullTxObjects>, <onCompleteObservable>) .subscribe(block -> { ... }); 复制代码或者,也可以在你再现最新的块后,通知新创建的后续块:Subscription subscription = web3j.catchUpToLatestAndSubscribeToNewBlocksObservable( <startBlockNumber>, <fullTxObjects>) .subscribe(block -> { ... }); 复制代码如上所述,并包含在块内的交易:Subscription subscription = web3j.catchUpToLatestAndSubscribeToNewTransactionsObservable( <startBlockNumber>) .subscribe(tx -> { ... }); 复制代码所有上述过滤器都是通过Web3JRX接口导出的。主题过滤器和EVM事件主题过滤器捕获在网络中发生的以太坊虚拟机(EVM)事件的细节。这些事件是由智能合约创建的,并存储在与智能合约相关联的交易日志中。solidity文档提供了EVM事件的良好概述。使用EthFilter类型指定希望应用于过滤器的主题。这可以包括希望应用过滤器的智能合约的地址。你还可以提供特定的主题进行筛选。其中单个主题表示智能合约上的索引参数:EthFilter filter = new EthFilter(DefaultBlockParameterName.EARLIEST, DefaultBlockParameterName.LATEST, <contract-address>) [.addSingleTopic(...) | .addOptionalTopics(..., ...) | ...]; 复制代码然后可以使用类似于上面的块和交易过滤器的语法创建该过滤器:web3j.ethLogObservable(filter).subscribe(log -> { ... }); 复制代码过滤器主题只能引用索引的Solidity事件参数。不可能对非索引事件参数进行筛选。此外,对于可变长度数组类型(如字符串和字节)的任何索引事件参数,它们的值的Keccak-256 hash 存储在EVM日志上。不可能使用它们的全部值来存储或筛选。如果创建一个没有与之相关联的主题的过滤器实例,则在网络中发生的所有EVM事件都将由过滤器捕获。操作组合标注除了send()和sendAsync之外,所有JSON-RPC方法在web3j中都实现了支持observable()方法来创建可观察的异步执行请求。这使得将JSON-RPC调用组合成新的函数是非常容易和直接的。例如, blockObservable本身由许多单独的JSON-RPC调用组成:public Observable<EthBlock> blockObservable( boolean fullTransactionObjects, long pollingInterval){ return this.ethBlockHashObservable(pollingInterval) .flatMap(blockHash -> web3j.ethGetBlockByHash(blockHash, fullTransactionObjects).observable()); } 复制代码在这里,我们首先创建一个可观察的,它提供每个新创建的块的块哈希的通知。然后,我们使用flatMap调用ethGetBlockByHash,以获得完整的块细节,这是传递给可观察者的订阅服务器的细节。进一步的例子请参阅ObservableIT,进一步举例说明。对于使用手动筛选器API的演示,可以查看EventFilterIT。web3j教程,主要是针对java和android程序员进行区块链以太坊开发的web3j开发详解。 以太坊教程,主要介绍智能合约与dapp应用开发,适合入门。 以太坊开发,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。 php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和事件等内容。 原文出处:web3j教程:过滤器(Filters)和事件(Events)
2023年03月24日
9 阅读
0 评论
0 点赞
2023-03-24
以太坊智能合约gas如何估计?
以太坊如何估计估算计算gas?Etherscan上transaction info中有个gas used by txn,结果跟remix给的结果以及geth中getTransactionReceipt的gasUsed给的结果都会是一致的,可以直接用geth或是remix模拟估算gas cost。之前一直没把这个问题搞清楚,所以干脆做个试验看一下.remix浏览器下方有个可执行的log页面,可以detail以及debug,非常方便。有gas cost的地方有两个地方,transaction cost以及 execution cost,這两个有什么不同呢?可以參考一下他们的源码。简单说一下: transaction cost指的是将交易送至ethereum blockchain所耗费的cost,是基于data size的大小,部署合约时就是基于合约內容的大小. execution cost指的是虚拟机(VM)执行所需的cost,而在部署合约时,会去执行建構子以及一些初始化的工作.在这里做一个简单的合约试验:contract Test { bytes32 public tmp; function test( bytes32 input, uint num ) constant returns (bytes32){ bytes32 result = input; for(uint i = 0; i < num; i++) { result = sha3(result); } } function set(bytes32 input, uint num) { tmp = test(input, num); } } 复制代码如果直接呼叫constant function的话,因为是由本身节点去计算不会更改到区块链上的值,是不会消耗gas的,但是如果是由一个一般合约(非constant function call)去呼叫一个constant function的話,因为让矿工来计算constant function,所以会消耗gas.上面的简单合约中,我让test函数对第一个bytes32参数做sha3,第二个uint参数代表做几次loop,我分別对set函数和test函数带入10以及1000的参数,結果如下.set(“0x63d7db5ce060b288ecf5390594d5969bc1a206ceeb24df31cffcc8876df5e44b”, 10) transaction cost:30628execution cost:6988 复制代码set(“0x63d7db5ce060b288ecf5390594d5969bc1a206ceeb24df31cffcc8876df5e44b”, 1000) transaction cost:196022 execution cost:172318 复制代码test(“0x63d7db5ce060b288ecf5390594d5969bc1a206ceeb24df31cffcc8876df5e44b”, 10) transaction cost:25663 (cost only applies when called by a contract) execution cost:2023 (cost only applies when called by a contract) 复制代码test(“0x63d7db5ce060b288ecf5390594d5969bc1a206ceeb24df31cffcc8876df5e44b”, 1000) transaction cost:191057(cost only applies when called by a contract) execution cost:167353(cost only applies when called by a contract) 复制代码ps:用transaction cost减去execution cost的话1, 3得到23640,2, 4得到23704大致上就是这样一个过程.发现参数设定成1000时,也会造成transaction cost的提高.(初步猜想加上ps的计算:transaction cost中是已经包含了execution cost,一并计算在最后要支付给miner的fee,因为每个相减结果都差不多)另外geth的estimateGas的之所以会不太准确是因为一些不确定性的operator操作会不同,比如说,在一个contract中,若是blockhash的尾数是奇数,他就去执行会消耗大量gas的合约,反之则去执行hello world合约,所以他的gas cost很大的情况下是一半一半.所以永远要记得设定一个合理的gas limit来防止自己遭受恶意攻击.另外建议可以参考traceTransaction指令,可以看每一個opcode的gas cost. 为了要确认矿工处理transaction的情况,在ropsten testnet上做个简单的试验.首先在ropsten faucet上拿一点儿ether来玩,然后在metamask上送出交易,因为ropsten是模拟pow的环境,所以我相信应该会是正确的数字.重要的话再说一次结论:Etherscan上transaction info中有个gas used by txn,结果跟remix给的结果以及geth中getTransactionReceipt的gasUsed给的结果都会是一致的,以后可以直接用geth或是remix模拟估算gas cost.参考资料:以太坊DApp开发实战入门可以加微信拉以太坊技术群聊。
2023年03月24日
7 阅读
0 评论
0 点赞
2023-03-24
开发者的以太坊入门指南 | Jeth 以太坊系列线下活动
本次活动文字整理链接智能合约全栈介绍 - Howard | Jeth 第一期以太坊智能合约 + DApp 从入门到上线:来自前端工程师的实战指南 - 王仕军 | Jeth 第一期详解 ERC20 代币及众筹 - 熊丽兵 | Jeth 第一期录像回放(哔哩哔哩)智能合约全栈介绍(视频回放) - Howard | Jeth 第一期以太坊智能合约 + DApp 从入门到上线:来自前端工程师的实战指南(视频回放) - 王仕军 | Jeth 第一期详解 ERC20 代币及众筹(视频回放) - 熊丽兵 | Jeth 第一期圆桌讨论环节(视频回放) | 掘金 Jeth 以太坊线下活动第一期???? 介绍Jeth 以太坊系列线下活动:《开发者的以太坊入门指南》来了!Jeth 是由掘金技术社区主办,以太坊社区基金会、以太坊爱好者与 ConsenSys 协办,围绕以太坊技术开发主题的系列线下活动。每期 Jeth 会邀请以太坊开发领域的优秀技术团队和工程师在线下分享技术干货。旨在为开发者提供线下技术交流互动机会,帮助开发者成长。门票免费第一期的 Jeth 线下分享我们邀请到以太坊布道师叶倍宏、ArcBlock 区块链开发工程师王仕军,以及登链科技CTO 熊丽兵,来给开发者分享一下以太坊的全栈架构,智能合约+DApp 的实战开发,以及 ERC20 代币及众筹的相关技术点,帮助有编程经验的开发者能够快速上手以太坊开发。线上直播地址:IT大咖说???? 日程时间:2018/06/10 14:00 - 17:00地点:北京市海淀区中关村创业大街 3W咖啡???? 本期嘉宾Howard 叶倍宏 - 量子链 DApp 平台核心工程师Howard 是《Deep Dive Into Ethereum Virtual Machine》一书的作者,该书深度剖析了 Solidity 和以太坊的原理。目前,Howard 任 Qtum 量子链 DApp 平台核心工程师,负责开发工具和数据库。Howard 在创业界拥有10年的产品开发经验,并且对构建去中心化产品充满热情。分享主题:《DApp 应用平台从前端到后端》通过这个分享,简单的智能合约案例来了解以太坊的全栈架构。当用户点击一个按钮时,背后会发生哪些网路请求,怎样通过共识,智能合约如何被执行,最终又怎样将数据存储于区块链。王仕军 - 以太坊布道师爱折腾、爱分享的前端老司机,有过以下作品:掘金专栏作者:王仕军,11250 人关注; 掘金小册作者:《区块链开发入门:从 0 到 1 构建基于以太坊智能合约的 ICO DApp》,709 人购买; 微信公众号前端周刊创办和维护者,2600 订阅; 高质量技术视频教程作者:async/await、styled-components,网盘下载量近 3000 次。 分享主题:《以太坊智能合约 + DApp 从入门到上线:来自前端工程师的实战指南》站在资深前端工程师的视角,从实战角度出发,讲解如何构建自动化的以太坊智能合约工作流,如何搭建 DApp 开发框架,如何完成 DApp 的构建和部署。熊丽兵 - 登链科技 CTO北京航空航天大学硕士,先后加入创新工场及猎豹移动,全面负责数款千万级用户开发及管理工作,2014年作为技术合伙人参与创建酷吧时代科技。2016年重心投入区块链技术领域,目前在登链科技任CTO,是全网访问量最大的区块链技术博客《深入浅出区块链》博主,对底层公链技术,区块链技术落地都有深入研究。分享主题:《详解 ERC20 代币及众筹》如何实现一个简单的代币,Solidity ERC20 代币接口分析及实现,如何实现挖矿、锁定等功能;给代币实现一个发行机制(众筹),以及代币合约漏洞分析。???? 入群与讲师讨论需要入群请添加稀土君微信 xitujun 回复 以太坊或者扫下面的二维码入群:???? 抽奖奖品???? 关于主办方掘金: 一个帮助开发者成长的社区。???? 协办方Ethereum Community Fund: 以太坊社区基金会以太坊爱好者: 最好的以太坊中文技术社区ConsenSys: Harness the power of Ethereum❤️ 合作伙伴???? 独家视频直播逐步更新中……???? FAQ活动收费么?Jeth 以太坊线下分享活动都不收费,但是为了保证参会者为开发者,需要对报名进行审核。活动会有直播么?Jeth 的 合作伙伴 IT大咖说 负责线上直播,直播地址会在群内发布。什么时候在 XXX 城市举办?Jeth 线下活动规划表,更明确的时间请加稀土君微信 xitujun 获取最新通告:7月 杭州 8月 北京 9月 上海 10月 北京 11月 杭州 12月 北京 我能和 Jeth 合作么?掘金欢迎媒体和社区朋友加入以太坊生态建设,欢迎各种形式的合作。联系请加微信:xitujun特别鸣谢本活动页面的布局和排版参考设计师丁一的 Sketch Meetup 活动报名页面。
2023年03月24日
9 阅读
0 评论
0 点赞
2023-03-24
一文聊透 Solidity 语法:助你成为智能合约专家
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第9篇文章,点击查看活动详情关于区块链和智能合约开发者的区别解释我发现很多人都表述不清楚区块链和智能合约。我认识几位程序员朋友,他们都自称是在做区块链开发,但实际上是在做智能合约的开发。大多数外行分不清楚区块链和智能合约我能理解,但是很多从事智能合约开发的程序员竟然也分不清楚,我不知道是不是表述问题还是理解问题。区块链是区块链,智能合约是智能合约,两者的关系就像是微信和微信小程序一样,一个是 App 开发,一个是小程序开发,根本不一样,不能混为一谈。据我了解,区块链的需求没那么多,特别是中国这个环境下。大多数区块链相关的程序员都是在做智能合约开发,而不是真的在开发区块链。区块链是可以用很多后端语言去开发的,比如用 Go、Node.js、Rust、Java 等。但是智能合约不可以随便选择编程语言,我这里讲的智能合约是指以太坊智能合约。目前它只能选择 Solidity、Vyper、YUL、YUL+ 和 Fe 这 5 种语言。其中 solidity 最受欢迎,大多数项目和开发者都是选择了 solidity。我们几乎可以说 solidity 是智能合约的首选编程语言。这篇文章会讲什么?这篇文章将会介绍我认为使用 Solidity 编写智能合约时 90% 以上的场景中能够用到的语法和特性。但是 Solidity 是一门完整的编程语言,想要把它彻底学明白,一篇文章肯定是不够的。因为很多语言都被写成了一厚厚地本书。不过通常写编程语言的书都会非常全体、体系化地介绍语言的全部,包括那些平时压根用不到的知识,或者一些已经落伍,语言设计上糟粕的部分。总体来说,通过一本厚厚的书来讲一门编程语言,多少是从研究的角度出发的,如果你只想快速用 Solidity 开发智能合约,不想把这门语言研究的这么透彻,那么本文很适合你。同时本文会拿 solidity 和一些面向对象的语言做对比,如果你完全不懂其他编程语言,那么本文不适合你。面向合约Solidity 的设计理念和面向对象编程语言很相似,不过 Solidity 是面相合约的编程语言,如果你有面向对象编程语言的开发经验,那么学习 Solidity 就没有那么难。Solidity 语言被设计为编写合约的语言,目前来说也只能写合约,所以它不像其他语言那样可以做很多事情。合约构成解读我们先来看一个最简单的合约构成,做一个整体的感受。// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract HelloWorld { address private owner; unit public state; modifier onlyOwner() { require(msg.sender == owner, "only owner"); _; } event StateChanged(unit state); constructor() public { owner = msg.sender; } function setState(uint _state) external onlyOwner { state = state; emit StateChanged(_state) } } 复制代码我简单解释下这个合约的代码,不会详细介绍。第 1 行是指定版本许可。第 2 行是指定使用的语言版本。第 4 行是声明一个名为 HelloWorld 的合约。第 5-6 行是状态变量,它们会永久存储在合约中。第 8 -11 行是函数修改器,它可以用在函数修饰符上,可以改变函数的行为。第 13 行是声明一个事件,事件可以被触发和监听。第 15-17 行是构造函数,在部署时会被调用。第 19-22 行是声明了一个名为 setState 的函数。版本solidity 有很多种版本,目前最新的版本是 8.x。但是在早期比较流行的是 5.x、6.x 这两个版本。solidity 的版本命名规范采用 。和其他大多数编程语言不同的是,solidity 的版本是直接写在源代码里的。在任意一个 sol 文件的最开始,都应该是版本代码。语法为:pragma solidity 0.8.0; 复制代码如果你用过 npm 的话,那这个版本语言一定不会陌生,因为 solidity 同样使用了 semver 版本规范。合约合约的概念有点像面向对象编程语言的类,属于一等公民。通过关键字 contract 创建。语法:contract MyContract { } 复制代码可以通过 new 关键字创建合约。new MyContract(); 复制代码继承面向对象的语言通常会使用 extends 关键字来继承,但是 solidity 没有这样做,它使用 is 来继承合约。contract MyContract1 { uint256 num = 2022; } contract MyContract2 is MyContract1 { } 复制代码子合约被部署时,会把所有父合约的代码一起打包,所以对父合约中函数的调用都属于内部调用。子合约可以隐式转换父合约,合约也可以显式转换为地址。address addr = address(c); 复制代码重写函数使用 override 关键字。父合约中支持重写的函数必须是 virtual 的。contract Parent { function fn() public virtual } contract Child is Parent { function fn() public override } 复制代码调用父合约中的方法,使用 super 关键字。contract Parent { function fn() public } contract Child is Parent { function fn2() public { super.fn(); } } 复制代码支持多重继承。contract Parent1 { function fn() public virtual } contract Parent2 { function fn() public virtual } contract Child is Parent1, Parent2 { function fn() public override(Parent1, Parent2) } 复制代码变量与基础类型变量是永久存储在合约中的值,通常用来记录业务信息。每个变量都需要声明类型,solidity 中的类型有如下几种:string:字符串类型 bool:布尔值,true/false。 uint:无符号整型,有 uint 和 uint8/16/32/64/128/256 几个类型。uint 是 uint256 的别名。 int:有符号整型,规则和 uint 一样。 bytes:定长字节数组。从 bytes1 到 bytes32,byte 是 bytes1 的别名。它和数组类似,通过下标获取元素,通过 length 获取成员数量。 address:地址类型。保存一个 20 字节的地址。 address payable:可支付的地址,有成员函数 transfer 和 send。 contract MyContract { string name = "" } 复制代码uint对于整型变量,我们可以通过 type(x).min 和 type(x).max 来获取某个类型的最大值和最小值。addressaddress payable 可以隐式转换到 address,但是 address 必须通过 payable(address) 这种方式显示转换。address 还可以显示转换为 uint160 和 bytes20。bytes 和 stringbytes 和 string 都是数组,而不是普通的值类型。bytes 和 byte[] 非常像,但是它在 calldata 和 memory 中会紧打包。紧打包的意思是将元素连续存储在一起,不会按照每 32 字节为一个单元进行存储。string 是变长 utf-8 编码的字节数组,和 bytes 不同的是它不可以用索引来访问。字符串没有操作函数,一般都是通过第三方 string 库来操作字符串。string 可以转换为 bytes,转换时是创建引用而不是创建拷贝。function stringToBytes() public pure returns (bytes memory) { string memory str = "hello"; bytes memory bts = bytes(str); return bts; } 复制代码由于 bytes 和 string 很相似,所以我们在使用它们时应该有对应的原则。对于任意长度的原始字节使用 bytes。 对于任意长度的 UTF-8 字符串使用 string。 当需要对字节数组长度进行限制时,应该使用 byte1-byte32 之间的具体类型。 合理使用 bytes 可以节省 Gas 费。变量修饰符我们也可以为变量指定访问修饰符。语法是 类型 访问修饰符(可选) 字段名。访问修饰符有三种:public:公开,外部可以访问,声明为 public 的话会自动生成 getter 函数。 internal:默认,只有合约自身和派生的合约可以访问。 private:只有合约自身可以访问。 solidity 中的变量与传统语言的变量有些不同。字符串的值默认不可以包含中文。如果要使用除了英文外的其他语言,必须加 unicode 前缀。 string name = unicode"小明"; 复制代码结构体使用关键字 struct 创建结构,有点类似 Go/C 的 struct,或者类似 TypeScript 中的 type/interface。struct User { string name; string password; uint8 age; bool state; } 复制代码初始化结构体和调用函数类似,参数的顺序和结构体的顺序保持一致。User user = User("章三", "123", 12, false); 复制代码访问某一个属性使用点号。user.name; 复制代码属性也可以直接赋值。user.name = "里斯"; 复制代码数组和 TypeScript 中的数组语法一致,语法是 type[]。User[] users; 复制代码访问数组元素,使用 array[index] 的方式。users[0]; 复制代码访问不存在的下标,会直接报错。在创建数组时可以声明长度,如果不声明,那就是可以动态调整大小的数组。uint256[10] nums; 复制代码数组具有 pop 和 push 方法,分别用于弹出一个元素和添加一个元素。但是它们不可以用在定长数组中。push 方法可以不传递参数,这时表示它添加一个该元素类型的零值。strs.push("1"); strs.pop(); 复制代码映射类似于很多语言中的 Map 结构。语法是 mapping(keyType => valueType)。mapping(address => User) userMapping; 复制代码key 的类型只允许是基本类型,不可以是复杂类型,比如合约、枚举、映射和结构体。value 的类型没有限制。访问 mapping 元素,使用 mapping[key] 的方式。userMapping[0x021221] 复制代码访问不存在的 key,会返回 value 类型的默认值。mapping 不可以作为公有函数的参数和返回值,只可以作为变量或者函数内的存储或者库函数的参数。声明为 public 的 mapping,会自动创建 getter 函数。KeyType 作为参数,ValueType 作为返回值。mapping 无法被遍历。不过有一些开源库用一种结构来实现了可遍历的 mapping。可以直接拿过来用。枚举枚举是创建用户自定义类型的一种方式。enum ActionChoices ActionChoices choice; 复制代码枚举可以和所有的整型显示相互转换,但是不能隐式转换。uint num = uint(choice); 复制代码从整型显示转换到枚举类型,会在运行时检查整数是否在枚举范围内,超过的话会导致异常。choice = ActionChoices(num); 复制代码枚举最少包含 1 个成员,最多可以包含 256 个成员。枚举默认值是第一个成员。枚举的数据表示和 C 语言是一样的,从 0 开始的无符号整数开始递增。构造函数部署合约时会由 EVM 自动调用构造函数,和常规的编程语言语法一致。contract MyContract { constructor () { } } 复制代码如果在构造函数中设置参数的话,那么在部署时需要传入对应参数的值。contract MyContract { constructor (uint256 initNum) { } } 复制代码构造函数不支持重载。如果一个合约没有构造函数,那么会采用默认构造函数,将所有变量初始化为类型对应的默认值。函数语法是 function(type param) [pure|view|payable] [returns(paramType)]可访问性标识符、状态标识符、函数修改器函数可以定义在合约之外,但是只能通过 internal 的形式访问。函数可以接受多个参数,也可以返回多个返回值。函数修改器可以放在函数声明中,具有修改函数行为的能力。modifier onlyOwner() { require(msg.sender == owner, "only owner"); _; } 复制代码常用的关键字有 require 和 _。require 有两个参数,第一个参数是一个 bool 值,如果为 false,那么就会触发错误,终止函数运行。第二个参数是当发生错误时的消息。_ 表示函数运行。使用函数修改器只需要在函数的修饰符部分添加修改器的名字即可,如果要添加多个修改器,使用空格隔开。function setState(uint _state) external onlyOwner m2 m3 { state = state; emit StateChanged(_state) } 复制代码函数修改器可以被继承。函数修饰符修饰符可以用在成员属性或者函数上,它决定了成员属性/函数的访问权限,共有 4 种:public:最大访问权限,任何人都可以调用。 private:只有合约内部可以调用,不可以被继承。 internal:子合约可以继承和调用。 external:外部可以调用,子合约可以继承和调用,当前合约不可以调用。 external 和 public 的函数是合约的成员变量,可以通过 fn.address 来获取地址,通过 .selector 来获取标识符,这也被称作函数选择器。函数调用函数分为内部函数与外部函数。内部函数只有在同一个合约内的函数可以内部调用,内部调用可以递归调用。函数调用在 EVM 中会被解释为简单地跳转,内存不会被清除。比如可以做斐波那契数列。contract MyContract { function fibonacci(uint256 n) public returns (uint256) { if (n == 1 || n == 2) { return 1; } return fibonacci(n - 2) + fibonacci(n - 1); } } 复制代码外部调用调用父合约的 external 方法和调用其他合约中的 external/public 方法,都属于外部调用。调用父合约的方法使用 this.fn();,调用外部合约的方法使用 contract.fn();。进行外部调用会通过消息调用,而不是简单跳转。接口与传统语言一样,使用关键字 interface。接口可以被合约继承。interface Token { function transfer(address recipient, uint amount) external; } contract MyToken is Token { function transfer(address recipient, uint amount) external override } 复制代码事件定义事件:event eventName(paramsType paramsName) 复制代码触发事件。emit eventName(params) 复制代码事件会被记录到区块链的 Log 中,区块链的 Log 分为索引和数据。我们可以指定最多 3 个参数为 indexed,表示它们可以被索引。前端可以通过 web3.js 来订阅和监听事件。事件也可以被继承。控制结构solidity 支持大多数传统编程语言的流程控制语句。比如 if、else、while、do、for、break、continue、return。但是不支持 goto 和 switch。solidity 支持 try/catch 做异常处理,但是只支持外部函数调用和合约创建调用。数据存储位置所有引用类型的数据(包括数组、结构体、mapping、合约等)都有三种存储位置。分别是:内存 memory:合约执行时的内存。 存储 storage:合约的永久存储。 调用数据 calldata:不可修改,函数的参数。和 memory 有些像,但和内存不在同一个位置。 直接声明在合约中的变量都会存储在 storage 中。声明为 external 的函数,参数必须存储在 calldata。在 storage 和 memory/calldata 之间进行复制,会创建独立的拷贝。memory 和 calldata 之间相互赋值不会创建拷贝,而是创建引用。storage 与本地 storage 之间的赋值也只会创建引用。contract MyContract { uint256[] arr1; // arr1 存储在 storage 中 // arr2 存储在 memory 中 function fn1(uint256[] memory arr2) public { // memory 赋值到 storage 中,创建拷贝 arr1 = arr2; // stoarge 赋值到 本地 storage 中,创建引用 uint256[] storage arr4 = arr1; // pop 会同时影响 arr1 arr4.pop(); // 清空 arr1,同时会影响 arr4 delete arr1; // storage 是静态分配内存,所以不可以直接从 memory 赋值到本地 storage 中 // arr4 = arr2; // 因为没有指向存储位置,所以无法重置指针 // delete arr4; // storage 之间传递引用 fn3(arr1); // storage 到 memory 会拷贝 fn4(arr1); } // arr3 存储在 calldata 中 function fn2(uint256[] calldata arr3) external function fn3(uint256[] storage arr5) internal pure function fn4(uint256[] memory arr6) public pure } 复制代码在使用数据时,要优先考虑放在 memory 和 calldata 中。因为 EVM 的执行空间有限。而且如果 storage 的占用很高,Gas 费也会很贵。单位solidity 中有两种单位。以太单位和时间单位。以太单位以太单位是以太坊独有的单位,在其他编程语言中没有这种单位。1 wei 等于 1。1 gwei 等于 1e9。1 ether = 1e18。用代码表示如下:assert(1 wei == 1); assert(1 gwei == 1e9); assert(1 ether == 1e18); 复制代码时间单位默认 1 等于 1 秒。solidity 支持以下时间单位:seconds:秒 minutes:分 hours:时 days:天 weeks:周 years:年,不推荐使用。 用代码表示如下:assert(1 seconds == 1); assert(1 minutes == 60 seconds); assert(1 hours == 60 minutes); assert(1 days == 24 hours); assert(1 weeks == 7 days); 复制代码错误处理与异常Solidity 使用状态恢复异常来处理错误。这种异常会撤销当前调用以及子调用中的状态变更,并且会向调用者标记错误。外部调用的异常可以被 try/catch 捕获。assertassert 用在我们认为不会出现错误的地方,它返回 Panic(uint256) 类型的错误。function buy(address payable addr) public { addr.transfer(1 ether); assert(addr.balance > 1 ether); } 复制代码requirerequire 通常用来条件判断,它会创建一个 Error(string) 类型的错误,或者是没有错误数据的错误。function buy(uint amount) public { require(amount < 1, "amount must be greater than 1"); } 复制代码revert可以用来标记错误并且退回当前调用。require 本身也会去调用 revert。function buy(uint amount) public { if(amount < 1) { revert(amount > 1, "amount must be greater than 1"); } } 复制代码区块和交易属性区块和交易属性都是以全局变量或者全局函数的形式存在的。我们可以直接访问它们。常见的属性如下:blockhash(uint blockNumber) returns (bytes32):获取指定区块的区块哈希,可用于最新的 256 个区块,不包含当前区块。 block.chainid:uint 类型,当前链的 id。 block.coinbase:address 类型,当前区块的矿工地址。 block.diffculty:uint 类型,当前区块的难度。 block.gaslimit:uint 类型,当前区块的 gas 限额。 block.number:uint 类型,当前区块号。 block.timestamp:uint 类型,从 unix epoch 到当前区块以秒计的时间戳。 gasleft() returns (uint256):剩余的 gas。 msg.data:bytes 类型,完整的 calldata。 msg.sender:address 类型,消息发送者(当前调用者)。 msg.sig:bytes4 类型,calldata 的前 4 个字节,也就是函数标识符。 msg.value:uint 类型,消息发送的 wei 数量。 tx.gasprice:uint 类型,当前交易的 gas 价格。 tx.origin:address payable 类型,交易发起者。 receive 和 fallbackreceive 是一个特殊的函数,一个合约可以包含最多一个 receive 函数。receive 没有 function 关键字,必须是 external payable 的。可以是 virtual 的,可以被重载,可以添加 modifier。我们给合约转账时,会去执行 receive 函数。如果转账时 receive 函数不存在,会去调用 fallback 函数。如果 fallback 函数也不存在,那么合约不可以通过正常转账来接受 ether。fallback 函数和 receive 类似,只能最多有一个 fallback 函数,必须是 external 的,可以是 virtual 的,可以被重载,可以添加 modifier。但 payable 是可选的。fallback 方法可以接受参数,也可以返回数据。如果调用某个合约的函数,但是这个函数不存在,会调用 fallback。contract MyContract { receive() external payable fallback() external } 复制代码我是代码与野兽,一位长期专注于 Web3 的探索者,同时也非常擅长 Web2.0 中的前后端技术。如果你对 Web3 感兴趣,可以关注我和我的专栏。我会持续更新更多 Web3 相关的高质量文章。
2023年03月24日
11 阅读
0 评论
0 点赞
2023-03-24
下一代的智能合约编程语言Move(五)
前言上一篇文章我们了解了Move语言的结构体与类型系统,这篇文章将会介绍Move中的所有权机制。所有权owershipMove虚拟机实现了类似Rust的所有权系统,有兴趣可以先了解一下Rust中的所有权系统。每一个变量都有自己的范围,当超出范围时,该范围内的变量也会被丢弃。我们已经在表达式的相关章节中看到了这种现象,记住一个变量只在自己的范围内生效。每一个包含变量的范围都是所有者,变量可以是在这个范围内通过let定义的,也可以是通过参数传递进这个范围的,在Move中只有函数能将变量传递进一个范围。每一个变量都只有一个拥有者,这意味着当一个变量被当作参数传递给一个函数时,这个函数就变成了这个变量新的所有者。script { use }::M; fun main() { // Module::T是一个结构体 let a: Module::T = Module::create(10); // 这时候变量a离开main函数的范围,进入M::value函数范围内 M::value(a); // 这时候main函数范围内已经没有a这个变量,编译会报错 M::value(a); } } 复制代码模块M的实现如下:module M { struct T public fun create(value: u8): T { T } //变量t传递给函数value,value函数拥有变量的所有权 public fun value(t: T): u8 { t.value } // 这时候函数范围结束,变量t被丢弃,不会再存在 } 复制代码move和copy首先我们需要了解Move VM是如何工作的,当我们传递参数给一个函数又发生了什么,在VM中有两个字节码指令,一个是MoveLoc,一个是CopyLoc,它们分别可以通过move和copy关键字使用。当一个变量被传递给其他函数时,它被使用MoveLoc移动,例子如下script { use }::M; fun main() { // Module::T是一个结构体 let a: Module::T = Module::create(10); // 这时候变量a离开main函数的范围,进入M::value函数范围内 M::value(move a); //变量a已经被废弃 } } 复制代码move关键字可以省略,这里仅仅为了说明如果想传递一个值给函数且想保存变量的值可以使用关键字copy。script { use }::M; fun main() { // Module::T是一个结构体 let a: Module::T = Module::create(10); M::value(copy a); //变量a依然存在 } } 复制代码以上我们通过copy关键字避免了变量被废弃,但是copy会增加内存使用,当copy非常大的数据时代价很大,在区块链中每个字节都会影响执行的代价,为了避免过大的额外开销,可以使用引用。引用很多编程语言都实现了引用,引用是变量的链接,通过引用可以将变量传递给程序其他部分而不用传递变量的值。引用(通过&)可以不需要所有权就可以获取到一个变量module M { struct T //传递一个引用而不是传递一个值 public fun value(t: &T): u8 { t.value } } 复制代码不可变的引用只能读取变量的值,不能改变变量的值,可变的引用可以读写变量的值。module M { struct T //返回一个非引用类型的值 public fun create(value: u8): { T } //不可变的引用只允许读 public fun value(t: &T): u8 { t.value } // 可变引用允许读写值 public fun change(t: &mut T, value: u8) { t.value = value; } } 复制代码Borrow检查Move中通过Borrow检查来控制程序中引用的使用,这样有助于避免出错。module Borrow { struct B struct A // 创建一个含有B的A public fun create(value: u64): A { A } } // 获得B的可变引用 public fun ref_from_mut_a(a: &mut A): &mut B { &mut a.b } // 改变B public fun change_b(b: &mut B, value: u64) { b.value = value; } } 复制代码script { use }::Borrow; fun main() { // 创建一个A let a = Borrow::create(0); // 通过A获取B的可变引用 let mut_a = &mut a; let mut_b = Borrow::ref_from_mut_a(mut_a); // 改变B Borrow::change_b(mut_b, 100000); // 获取另一个A的可变引用 let _ = Borrow::ref_from_mut_a(mut_a); } } 复制代码上面代码可以成功编译运行,不会报错。这里究竟发生了什么呢?首先,我们使用 A 的可变引用(&mut A)来获取对其内部 struct B 的可变引用(&mut B)。然后我们改变 B。然后可以再次通过 &mut A 获取对 B 的可变引用。但是,如果我们交换最后两个表达式,即首先尝试创建新的 &mut A,而 &mut B 仍然存在,会出现什么情况呢?let mut_a = &mut a; let mut_b = Borrow::ref_from_mut_a(mut_a); let _ = Borrow::ref_from_mut_a(mut_a); Borrow::change_b(mut_b, 100000); 复制代码此时编译器会报错 ┌── /scripts/script.move:10:17 ─── │ 10 │ let _ = Borrow::ref_from_mut_a(mut_a); │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid usage of reference as function argument. Cannot transfer a mutable reference that is being borrowed · 8 │ let mut_b = Borrow::ref_from_mut_a(mut_a); │ ----------------------------- It is still being mutably borrowed by this reference │ 复制代码该代码不会编译成功。为什么?因为 &mut A 已经被 &mut B 借用。如果我们再将其作为参数传递,那么我们将陷入一种奇怪的情况,A 可以被更改,但 A 同时又被引用。 结论如下编译器通过所谓的"借用检查"(最初是Rust语言的概念)来防止上面这些错误。编译器通过建立"借用图",不允许被借用的值被"move"。这就是 Move 在区块链中如此安全的原因之一。 可以从引用创建新的引用,老的引用将被新引用"借用"。可变引用可以创建可变或者不可变引用,而不可变引用只能创建不可变引用。 当一个值被引用时,就无法"move"它了,因为其它值对它有依赖。 取值可以通过取值运算*来获取引用所指向的值。值运算实际上是产生了一个副本,要确保这个值具有 Copy ability。module M { struct T has copy // value t here is of reference type public fun deref(t: &T): T { *t } } 复制代码取值运算不会将原始值 move 到当前作用域,实际上只是生成了一个副本有一个技巧用来复制一个结构体的字段:就是使用*&,引用并取值。我们来看一个例子module M { struct H has copy struct T // ... // we can do it even from immutable reference! public fun copy_inner(t: &T): H { *&t.inner } } 复制代码基本类型基本类型非常简单,它们不需要作为引用传递,缺省会被复制。当基本类型的值被传给函数时,相当于使用了copy关键字,传递进函数的是它们的副本。当然你可以使用move关键字强制不产生副本,但是由于基本类型的大小很小,复制它们其实开销很小,甚至比通过引用或者"move"传递它们开销更小。最后这篇文章主要介绍了Move中的所有权,更多文章可以关注公众号QStack。
2023年03月24日
6 阅读
0 评论
0 点赞
2023-03-24
Remix IDE 使用与 VSCode 搭建 Solidity 开发环境
Remix IDE 基本使用智能合约的默认编辑器是 Remix,它默认是运行在浏览器中的。我们打开 remix.ethereum.org 就可以看到 Remix 编辑器了。需要注意这个域名是 Remix 的唯一域名。它的功能比较简单,和 VSCode 有些像,我简单介绍一下它的主要功能。左侧四个菜单栏是它的主要功能。第一个是文件夹。 第二个是搜索栏。 第三个是编译。 第四个是部署。 文件夹我们通常不会直接在这里进行开发,而是在 VSCode 中进行开发,然后将本地的代码连接到浏览器的 Remix 中。但是可以在这里写一些测试或者 Demo。搜索栏搜索栏和 VSCode 的搜索栏基本上没有什么区别,通常就是全局搜索一些关键字,或者全局替换之类的。编译编译是比较重要的一部分,这里我要多讲一下。compiler:首先是选择 solidity 的版本,编译版本要和代码版本相对应。 solidity 的版本是可以有区间的,不过建议锁定一个版本,因为如果要在区块链浏览器中公开智能合约代码的话,必须填写编译和部署的参数。如果设置成区间,很容易忘记编译时所选择的版本,导致无法公开智能合约代码。include nightly builds:是夜间编译,通常是指每晚编译,也就是指每天晚上程序员下班后进行的编译,这时进行编译的代码是没有完成的。我们不需要勾选。 auto compile:自动编译,当文件的内容发生变化时会自动编译,如果项目比较小,可以打开,但是如果项目比较大,那么不建议打开。因为会让浏览器卡顿。而且我们可以按 ctrl+s 的快捷键来编译,不需要这个功能。 hide warnings:隐藏警告。这个选项建议始终打开,显示警告可以帮助我们避免很多问题。 Advanced Configurations 是编译的高级选项。language:是编程语言,默认 solidity,不需要修改。 EVM Version:这个是 EVM 的版本,暂时也不需要修改。 Enable Optimization:这个选项是代码优化。代码编译是有上限的,如果项目提及非常大,超过了这个限制,就无法编译成功。开启代码优化后,可以减小代码的体积,让智能合约容易通过。并且还可以减少合约部署的 gas 费和外部调用的 gas 费,所以我们最好打开这个选项。代码优化的级别是数字,选择默认 200 就可以了。 Use configuration file:我们也可以自己编写编译配置文件。等后面再说这个,现在不开启。 选择完配置参数,就可以点击 Compile 按钮进行编译了。编译完成后编译图标会出现一个绿色的对勾,表示编译成功。同时还会输出 ABI 和 Bytecode,这两个东西我会在后面的文章中讲到。部署部署也非常重要,我也对这部分进行详细的介绍。Eevironment:虚拟机环境。虚拟机有很多,默认的是 Remix VM,也就是浏览器虚拟机,在之前的版本中也叫做 JavaScript VM。当浏览器刷新时,旧链会被清除。我们做一些简单的测试可以使用它。如果合约中有一些买币卖币的逻辑需要测试,那么就需要选择 Injected Provider-Metamask。这样就可以连接到 Metamask 钱包,在真实网络的测试链中进行测试。 Account 是一些和当前环境关联的账户列表,里面会包含一些 Ether,可以用来测试。 Gas limit:智能合约可以从用户手中最大的 gas 费用限制,默认是 3000000,目的是为了防止智能合约程序出错,收取过高的 gas 费用。通常不建议修改。 Value:发送到合约或调用函数所需要支付的 ETH、WEI、GWEI 等,默认是 0,不需要修改。 Contract:就是我们需要选择的合约。如果有多个合约,就去选择要部署的那个合约。 Publish to IPFS:发布到星际网络,不需要开启。 配置好之后就可以点击 Deploy 进行部署了。部署成功后 Deployed Contracts 这部分会有部署好的智能合约地址。它会有一些蓝色的按钮和黄色的按钮。蓝色按钮表示获取链上数据,黄色按钮表示修改链上数据。修改链上数据需要支付 gas 费。使用 VSCode 编写 Solidity 代码Solidity 的官方默认编辑器是 Remix,但它是运行在浏览器中的,浏览器并不是非常稳定,比如莫名其妙的崩溃导致代码丢失。虽然 Remix 也有桌面版本,在这里下载:github.com/ethereum/re…,但是使用的人并不多,可能是因为 Remix 并不是专业做代码编辑器的,编码体验和生态系统都不够完善。如果能够使用传统的、具有更加成熟生态的编辑器,那么在开发体验和开发效率上都会有更好的体验。下面这部分内容将会介绍如何使用本地的 VSCode 编辑器进行 Solidity 智能合约的开发。插件安装在 VSCode 中编写 Solidity 代码,并且导入到 Remix,需要安装 Solidity 和 Ethereum Remix 这两个插件。Soliditysolidity 是一门编程语言,那么一样需要代码高亮、代码提示这些功能。这个插件提供了一门编程语言所需要的基本功能,以及编译合约等功能。F5 编译当前合约,Cmd+F5 编译所有合约。安装地址:marketplace.visualstudio.com/items?itemN…安装完成后,可以编写一个 sol 文件,代码高亮就代表着安装成功。Ethereum Remix我们在本地写完智能合约后,需要将本地的项目导入到 Remix 中进行部署。这在大型项目中比较有用,因为大型项目的目录结构会比较复杂,比如会包含很多文件夹和一些库。安装地址:marketplace.visualstudio.com/items?itemN…安装完成后,左侧会有一个 Remix 的图标。点进去,可以看到以下内容。选择最下面的 Start remixd client,就可以连接到浏览器的 Remix。回到浏览器的 Remix,在 Workspaces 中选择 connect to localhost。选择之后会弹出一个弹窗,警告我们 remix 的版本必须是最新版本。我们点击 connect,就连接成功了。连接成功后,Strat remixd client 前面的图标也会变成绿色。
2023年03月24日
8 阅读
0 评论
0 点赞
2023-03-24
Java Web3J 使用指南
Web3J 是一个轻量级、高度模块化、反应式、类型安全的 Java 和 Android 库,用于处理智能合约并与以太坊网络上的客户端(节点)集成。这使您可以使用以太坊区块链,而无需为平台编写自己的集成代码的额外开销。提供的功能基于 HTTP 和 IPC 的以太坊 JSON-RPC 客户端 API 的完整实现 以太坊钱包支持 自动生成 Java 智能合约包装器,以从本机 Java 代码创建、部署、交易和调用智能合约(支持 Solidity 和 Truffle 定义格式) 以太坊名称服务 (ENS) 支持 支持 Alchemy 和 Infura,因此您不必自己运行以太坊客户端 安卓兼容 命令行工具 如何使用<dependency> <groupId>org.web3j</groupId> <artifactId>core</artifactId> <version>4.8.7</version> </dependency> 复制代码public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { Web3j client = Web3j.build(new HttpService("https://ropsten.infura.io/v3/You Infura Project Id")); Web3ClientVersion clientVersion = client.web3ClientVersion().sendAsync().get(); System.out.println(clientVersion.getWeb3ClientVersion()); // => Geth/v1.10.15-omnibus-hotfix-f4decf48/linux-amd64/go1.17.6 } } 复制代码使用 Web3J 获取以太坊账户余额public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { Web3j client = Web3j.build(new HttpService("https://ropsten.infura.io/v3/You Infura Project Id")); EthGetBalance ethGetBalance = client.ethGetBalance( "0x64f44b31ad0ed4537f94a5c084cfba8945463345", DefaultBlockParameterName.fromString(DefaultBlockParameterName.LATEST.name()) ).sendAsync().get(); System.out.println(ethGetBalance.getBalance()); // => 741270235881990866 } } 复制代码使用 Web3J 获取当前的 Gas 价格public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { Web3j client = Web3j.build(new HttpService("https://ropsten.infura.io/v3/You Infura Project Id")); EthGasPrice ethGasPrice = client.ethGasPrice().sendAsync().get(); System.out.println(ethGasPrice.getGasPrice()); // => 41493167936 } } 复制代码使用 Web3J 通过交易哈希获取交易详情public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { Web3j client = Web3j.build(new HttpService("https://ropsten.infura.io/v3/You Infura Project Id")); String transactionHash = "0x9030edd43f8ae6c4ed49bcbc11dd7d6f6ce2798e8bb1c5ea4f1e130780fec74a"; EthGetTransactionReceipt ethGetTransactionReceipt = client.ethGetTransactionReceipt(transactionHash).sendAsync().get(); TransactionReceipt transactionReceipt = ethGetTransactionReceipt.getTransactionReceipt().orElseThrow(RuntimeException::new); System.out.println(transactionReceipt); // => TransactionReceipt], logsBloom='0x80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000008000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000010000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000002000000000000000200000000000000000000000000000000000020000000000200000000000000000000000000000000000000000000000000000000', revertReason='null', type='0x2', effectiveGasPrice='0x15fb4c0517'} } } 复制代码使用 Web3J 订阅新的区块Web3J 的函数式编程的特性让我们设置观察者很容易,我们可以设置订阅者,监听链上发生的一些事情,如出块,交易,日志等。public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException, ConnectException { WebSocketService webSocketService = new WebSocketService("wss://ropsten.infura.io/ws/v3/You Infura Project Id", true); webSocketService.connect(); Web3j client = Web3j.build(webSocketService); Disposable subscribe = client.replayPastBlocksFlowable(DefaultBlockParameterName.fromString("earliest"), true).subscribe(ethBlock -> { System.out.println(ethBlock.getBlock()); // => org.web3j.protocol.core.methods.response.EthBlock$Block@39fcf950 }); } } 复制代码使用 Web3J 订阅新的交易public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException, ConnectException { WebSocketService webSocketService = new WebSocketService("wss://ropsten.infura.io/ws/v3/You Infura Project Id", true); webSocketService.connect(); Web3j client = Web3j.build(webSocketService); Disposable subscribe = client.replayPastTransactionsFlowable(DefaultBlockParameterName.fromString("earliest")).subscribe(transaction -> { System.out.println(transaction); // => org.web3j.protocol.core.methods.response.EthBlock$TransactionObject@47b0a71a }); } } 复制代码使用 Web3J 订阅新的合约事件public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException, ConnectException { WebSocketService webSocketService = new WebSocketService("wss://ropsten.infura.io/ws/v3/You Infura Project Id", true); webSocketService.connect(); Web3j client = Web3j.build(webSocketService); Disposable subscribe = client.ethLogFlowable( new EthFilter( DefaultBlockParameterName.EARLIEST, DefaultBlockParameterName.LATEST, // 合约地址 "0x7b52aae43e962ce6a9a1b7e79f549582ae8bcff9" ) ).subscribe(event -> { System.out.println(event); // => Log }, Throwable::printStackTrace); } } 复制代码使用 Web3J 签名并发送交易public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException, ConnectException { Web3j client = Web3j.build(new HttpService("https://ropsten.infura.io/v3/You Infura Project Id")); // 获取 nonce 值 EthGetTransactionCount ethGetTransactionCount = client .ethGetTransactionCount("0x64f44b31ad0ed4537f94a5c084cfba8945463345", DefaultBlockParameterName.PENDING) .sendAsync().get(); BigInteger nonce = ethGetTransactionCount.getTransactionCount(); System.out.println(nonce); // 构建交易 RawTransaction etherTransaction = RawTransaction.createEtherTransaction( nonce, client.ethGasPrice().sendAsync().get().getGasPrice(), DefaultGasProvider.GAS_LIMIT, "0x64f44b31ad0ed4537f94a5c084cfba8945463345", Convert.toWei("0.001", Convert.Unit.ETHER).toBigInteger() ); System.out.println(etherTransaction); // 加载私钥 Credentials credentials = Credentials.create("You Private Key"); // 使用私钥签名交易并发送 byte[] signature = TransactionEncoder.signMessage(etherTransaction, credentials); String signatureHexValue = Numeric.toHexString(signature); EthSendTransaction ethSendTransaction = client.ethSendRawTransaction(signatureHexValue).sendAsync().get(); System.out.println(ethSendTransaction.getResult()); } } 复制代码使用 Web3J 生成合约包装类Web3J 可以生成智能合约的包装类,方便使用纯 Java 代码与合约进行交互。$web3j generate solidity -a ./contract.abi -o ./ -p com.contract.proxy _ _____ _ | | |____ (_) __ _____| |__ / /_ \ \ /\ / / _ \ '_ \ \ \ | \ V V / __/ |_) |.___/ / | \_/\_/ \___|_.__/ \____/| | _/ | |__/ by Web3Labs Generating com.contract.proxy.Contract ... Warning: Duplicate field(s) found: [FUNC_SAFETRANSFERFROM]. Please don't use names which will be the same in uppercase. File written to . $tree com com └── contract └── proxy └── Contract.java # 包装类 2 directories, 1 file 复制代码使用 Web3J 与合约进行交互public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { Web3j client = Web3j.build(new HttpService("https://ropsten.infura.io/v3/You Infura Project Id")); Credentials credentials = Credentials.create("You Private Key"); BigInteger currentGasPrice = client.ethGasPrice().sendAsync().get().getGasPrice(); Contract foundersKeyContract = Contract.load("0x7b52aae43e962ce6a9a1b7e79f549582ae8bcff9", client, credentials, new DefaultGasProvider() { /** * 使用动态获取的 Gas Price * @return */ @Override public BigInteger getGasPrice() { return currentGasPrice; } }); BigInteger balance = foundersKeyContract.balanceOf("0x64f44b31ad0ed4537f94a5c084cfba8945463345").send(); System.out.println(balance); // => 41493167936 } } 复制代码
2023年03月24日
44 阅读
0 评论
0 点赞
2023-03-24
iOS 部署智能合约
废话因为项目业务原因,需要在新的项目中部署以太坊的智能合约,关于这方面安卓方面貌似走在了前面。 web3j 的存在实在是喜人,而 iOS版本也是有的,功能貌似没有那么强大,对付平常的区块链交互是足够的了。但是iOS这个他没有仔细的说明,我研究了一下其实部署起来挺简单. 年纪大了搞过的东西很容易忘记,于是在这里记录一下.一来自己以后也可以回顾,二来为有需要的人略尽绵薄之力语歌博客使用 web3swift 部署以太坊智能合约1.配置服务a. IP地址配置以太坊的服务器地址: 我这里是本地IP地址struct Api { static let host = "http://192.168.6.66:6666" static func map(path: String) -> String { return host + path } static var chainUrl: String { return map(path: "") } } 复制代码b. abiString部署合约会需要public let abiString = "[],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},],\"name\":\"setFlagData\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" 复制代码c. byteCode这个是一串很长的东西,合约生产的。没有的记得找写合约的要.我这里的例子:public let BINARY = "606060409081526003805460a060020a61ffff021916905560006004558051908101604052600981527f4c696e67546f6b656e0000000000000000000000000000000000000000000000602082015260059080516100619291602001906100cf565b5060408051908101604052600481527f4c494e4700000000000000000000000000000000000000000000000000000000602082015260069080516100a99291602001906100cf565b50601260075560038054600160a060020a03191633600160a060020a031617905561016a565b82805460018160011615610100020316600290049若干省略号复制代码d. 集成 podpod 'web3swift', '~> 0.8.0' 复制代码2.部署前奏a. 创建账户,web3swift 的示例中有,同学可以仔细研究研究,我这里就简单过。上代码:class YYGKey { var key_password = "BANKEXFOUNDATION" var storeManager: KeystoreManager? { return keyStoreManager() } var keyAddress: EthereumKeystoreV3? { return addressManage() } convenience init(password: String) { self.init() self.key_password = password } func addressManage() -> EthereumKeystoreV3? { guard let userDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else let storeManager = KeystoreManager.managerForPath(userDir + "/keystore") if let manage = storeManager, let address = manage.addresses { return address.isEmpty ? creatAddress(dir: userDir) : getAddress(manage: manage) } return nil } func creatAddress(dir: String) -> EthereumKeystoreV3? { let ks = try! EthereumKeystoreV3(password: "BANKEXFOUNDATION") if let ks_ = ks { let keydata = try! JSONEncoder().encode(ks_.keystoreParams) FileManager.default.createFile(atPath: dir + "/keystore"+"/key.json", contents: keydata, attributes: nil) } return ks } func getAddress(manage: KeystoreManager) -> EthereumKeystoreV3? { if let address = manage.addresses, let first = address.first { return manage.walletForAddress(first) as? EthereumKeystoreV3 } return nil } func keyStoreManager() -> KeystoreManager? { guard let userDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else return KeystoreManager.managerForPath(userDir + "/keystore") } } 复制代码b. 连接到部署的服务器class YYG { var chain: web3? { guard let url = URL(string: Api.chainUrl), let web = Web3.new(url) else return web } static let yyg = YYG() private init() } 复制代码3.开始部署需要的数据EthereumKeystoreV3 EthereumAddress web3 byteCode web3contract Web3Options TransactionIntermediate KeystoreManager 上代码:struct YYGOperate { typealias result = Result<[String : String], Web3Error> typealias res = [String : String] func contractsResult(message: @escaping (Bool,res?) -> Void) { if let resul = smartContracts() { switch resul { case .success(let res): DispatchQueue.main.async { message(true, res) } case .failure(let error): message(false, nil) print(error) } } else { message(false, nil) } } private func smartContracts() -> result? { guard let manage = YYGKey().addressManage(),let addresss = manage.addresses,let address = addresss.first, let chain = YYG.yyg.chain else chain.provider.network = nil chain.addKeystoreManager(YYGKey().keyStoreManager()) guard let contract = chain.contract(abiString, at: nil, abiVersion: 2), let byteCode = Data.fromHex(BINARY) else var options = Web3Options.defaultOptions() options.from = address options.gasLimit = BigUInt(3000000) let inter = contract.deploy(bytecode: byteCode, options: options) guard let intermediate = inter else return intermediate.send(password: "BANKEXFOUNDATION", options: options) } } 复制代码3.执行YYGOperate().contractsResult { state, res in if state } 复制代码结果:Transaction Nonce: 10 Gas price: 5000000000 Gas limit: 1111340 To: 0x Value: 0 Data: 0x606060409081526003805460a060020a61ffff02191690556090810183818151815260200191508051906020019080838360005b8381101561017657808201518382015260200161015e565b50、5260200160405180910390a35050505050565b6000828201610acc82fd76404b5c55184a2e555312d3353bcb43e75666dd36d0af562b0c220029a165627a7a72305820f3efa245dad6ee48cc6d771a54067fb6e3b17b0caba5ecd4eed13aca2b1c00960029 v: 28 r: 79879464740714101996923170089344479265591202785750775462035438111705924396692 s: 39143622105442894879914151031821436277241889279036648521816051027427579390397 Intrinsic chainID: nil Infered chainID: nil sender: Optional("0x9E40Fb081777c232aBafc256165772572c057F95") hash: Optional("0xcc16c3a245e4fd27b85e53b8cb6d0ac64028b60e61ca3de938e896e95202bde3") 复制代码授人以鱼不如授人以渔学习资料以太坊智能合约 web3swift 以太坊 以太坊官方文档 安卓web3j 学习区块链全部资料
2023年03月24日
6 阅读
0 评论
0 点赞
2023-03-24
JavaScript开发区块链只需200行代码
JavaScript开发一个简单的区块链只需200行代码。通过JavaScript的开发实现过程,你将理解区块链是什么:区块链就是一个分布式数据库,存储结构是一个不断增长的链表,链表中包含着许多有序的记录。然而,在通常情况下,当我们谈到区块链的时候也会谈起使用区块链来解决的问题,这两者很容易混淆。像流行的比特币和以太坊这样基于区块链的项目就是这样。“区块链”这个术语通常和像交易、智能合约、加密货币这样的概念紧紧联系在一起。这就令理解区块链变得不必要得复杂起来,特别是当你想理解源码的时候。下面我将通过 200 行 JS 实现的超级简单的区块链来帮助大家理解它,我给这段代码起名为 NaiveChain。你可以在Github 查看更多的技术细节。区块链块结构第一个逻辑步骤是决定块结构。为了保证事情尽可能的简单,我们只选择最必要的部分:index(下标)、timestamp(时间戳)、data(数据)、hash(哈希值)和 previous hash(前置哈希值)。这个块中必须能找到前一个块的哈希值,以此来保证整条链的完整性,JavaScript代码如下:class Block { constructor(index, previousHash, timestamp, data, hash) { this.index = index; this.previousHash = previousHash.toString(); this.timestamp = timestamp; this.data = data; this.hash = hash.toString(); } } 复制代码区块链块哈希为了保存完整的数据,必须哈希区块。SHA-256会对块的内容进行加密,记录这个值应该和“挖矿”毫无关系,因为这里不需要解决工作量证明的问题。JavaScript代码如下:var calculateHash = (index, previousHash, timestamp, data) => { return CryptoJS.SHA256(index + previousHash + timestamp + data).toString(); }; 复制代码区块链块的生成要生成一个块,必须知道前一个块的哈希值,然后创造其余所需的内容(= index, hash, data and timestamp)。块的data部分是由终端用户所提供的。JavaScript代码如下:var generateNextBlock = (blockData) => { var previousBlock = getLatestBlock(); var nextIndex = previousBlock.index + 1; var nextTimestamp = new Date().getTime() / 1000; var nextHash = calculateHash(nextIndex, previousBlock.hash, nextTimestamp, blockData); return new Block(nextIndex, previousBlock.hash, nextTimestamp, blockData, nextHash); }; 复制代码区块链块的存储内存中的Javascript数组被用于存储区块链。区块链的第一个块通常被称为“起源块”,是硬编码的。JavaScript代码如下:var getGenesisBlock = () => { return new Block(0, "0", 1465154705, "my genesis block!!", "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7"); }; var blockchain = [getGenesisBlock()]; 复制代码确认块的完整性在任何时候都必须能确认一个区块或者一整条链的区块是否完整。在我们从其他节点接收到新的区块,并需要决定接受或拒绝它们时,这一点尤为重要。JavaScript代码如下:var isValidNewBlock = (newBlock, previousBlock) => { if (previousBlock.index + 1 !== newBlock.index) { console.log('invalid index'); return false; } else if (previousBlock.hash !== newBlock.previousHash) { console.log('invalid previoushash'); return false; } else if (calculateHashForBlock(newBlock) !== newBlock.hash) { console.log('invalid hash: ' + calculateHashForBlock(newBlock) + ' ' + newBlock.hash); return false; } return true; }; 复制代码选择最长的区块链任何时候在链中都应该只有一组明确的块。万一冲突了(例如:两个结点都生成了72号块时),会选择有最大数目的块的链。JavaScript代码如下:var replaceChain = (newBlocks) => { if (isValidChain(newBlocks) && newBlocks.length > blockchain.length) { console.log('Received blockchain is valid. Replacing current blockchain with received blockchain'); blockchain = newBlocks; broadcast(responseLatestMsg()); } else { console.log('Received blockchain invalid'); } }; 复制代码与其他结点的通信结点的本质是和其他结点共享和同步区块链,下面的规则能保证网络同步。当一个结点生成一个新块时,它会在网络上散布这个块。 当一个节点连接新peer时,它会查询最新的block。 当一个结点遇到一个块,其index大于当前所有块的index时,它会添加这个块到它当前的链中, 或者到整个区块链中查询这个块。 我没有采用自动发现peer的工具。peers的位置(URL)必须是手动添加的。节点控制在某种程度上用户必须能够控制节点。这一点通过搭建一个HTTP服务器可以实现。JavaScript代码如下:var initHttpServer = () => { var app = express(); app.use(bodyParser.json()); app.get('/blocks', (req, res) => res.send(JSON.stringify(blockchain))); app.post('/mineBlock', (req, res) => { var newBlock = generateNextBlock(req.body.data); addBlock(newBlock); broadcast(responseLatestMsg()); console.log('block added: ' + JSON.stringify(newBlock)); res.send(); }); app.get('/peers', (req, res) => { res.send(sockets.map(s => s._socket.remoteAddress + ':' + s._socket.remotePort)); }); app.post('/addPeer', (req, res) => { connectToPeers([req.body.peer]); res.send(); }); app.listen(http_port, () => console.log('Listening http on port: ' + http_port)); }; 复制代码用户可以用下面的方法和节点互动:列出所有的块 用用户提供的内容创建一个新的块 列出或者新增peers 下面这个Curl的例子就是最直接的控制节点的方法:#get all blocks from the node curl http://localhost:3001/blocks 复制代码系统架构需要指出的是,节点实际上展现了两个web服务器:一个(HTTP服务器)是让用户控制节点,另一个(Websocket HTTP服务器)。总结创造 NaiveChain 的目的是为了示范和学习,因为它并没有“挖矿”算法(PoS或PoW),不能被用于公用网络,但是它实现了区块链运作的基本特性。其他更多内容也可以访问这个以太坊博客。如果你希望马上开始学习以太坊DApp开发,可以访问汇智网提供的快速有效在线互动教程:区块链新手以太坊DApp实战开发入门 区块链进阶去中心化电商平台实战开发
2023年03月24日
5 阅读
0 评论
0 点赞
2023-03-24
区块链上编程:DApp 开发简介
当你开始探索区块链开发,需要了解到这些。一、DApp介绍什么是 DApp?DApp 是 Decentralized Application 的简称,及去中心化应用。在某种程度上,比特币可以说是出现的第一个 DAPP,因为它是完全开源的,为贡献者提供奖励回报,不受一个中央机构的控制,并使用区块链作为支撑技术。 区块链,作为一个基础设施,提供了分布式的去中心化可信数据库,人们可以基于此,可以开发各种应用,适用于不同的场景。 简单来说,DAPP 和普通的 App 原理一样,除了他们是完全去中心化的,由类似以太坊网络本身自己的节点来运作的 DAPP,不依赖于任何中心化的服务器,DAPP 是去中心化的,可以完全自动地运行。 目前 DApp 通常指代基于以太坊或者 EOS 上的智能合约开发的相关应用。DApp 运行原理DApp 底层区块链开发平台就好比手机的 iOS 和 Android 系统,是各种 DApp 的底层生态环境,DApp 就是底层区块链平台生态上衍生的各种分布式应用,也是区块链世界中的基础服务提供方,DApp 于区块链,就好比 APP 之于 iOS 和 Android。什么是智能合约?如果把区块链看做是一个数据库,数据源, 智能合约基本上就是一段数据库操作脚本, 它决定了你如何在区块链上存储数据,修改数据。DApp应用案例前往这里可查看 DApp 的行业最新动态:www.stateofthedapps.com/cryptokitties 加密猫Fomo3D智能合约开发简介智能合约是代码(它的功能)和数据(它的状态)的集合,存在于以太坊区块链的特定地址。 智能合约账户能够在彼此之间传递信息,进行图灵完备的运算。智能合约依靠被称作以太坊虚拟机(EVM) 字节代码(以太坊特有的二进制格式)上的区块链运行。智能合约使用诸如 Solidity 等高级语言写成,然后编译成字节代码上传到区块链上。智能合约开发流程大概有以下步骤:编写智能合约(如基于 solidity) 测试智能合约,在测试网络或者私有链进行合约的功能测试 编译和发布合约,将合约部署到链上 操作合约,利用诸如 web3.js 等接口,通过访问智能合约的地址,来调用和操作智能合约。 结构示意图:智能合约的开发流程图:SoliditySolidity 是一种语法类似 JavaScript 的高级语言。它被设计成以编译的方式生成以太坊虚拟机代码。代码片段:pragma solidity ^0.4.22; contract helloWorld { function renderHelloWorld () public pure returns (string) { return 'helloWorld'; } } 复制代码ERC-20最著名的智能合约,想必大家都听过,那就是 ERC20。 ERC-20 是一种代币的标准协议,简单地说,任何 ERC-20 代币都能立即兼容以太坊钱包(几乎所有支持以太币的钱包,包括 MIST、imToken 等),由于交易所已经知道这些代币是如何操作的,它们可以很容易地整合这些代币。这就意味着,在很多情况下,这些代币都是可以立即进行交易的。一个基于 ERC-20 的代币包含以下接口:contract ERC20Interface { function totalSupply() public constant returns (uint); function balanceOf(address tokenOwner) public constant returns (uint balance); function allowance(address tokenOwner, address spender) public constant returns (uint remaining); function transfer(address to, uint tokens) public returns (bool success); function approve(address spender, uint tokens) public returns (bool success); function transferFrom(address from, address to, uint tokens) public returns (bool success); event Transfer(address indexed from, address indexed to, uint tokens); event Approval(address indexed tokenOwner, address indexed spender, uint tokens); } 复制代码Solidity开发环境介绍下面我会粗略的引入介绍一下 Solidity 智能合约相关开发环境跟工具,这些都是目前智能合约开发中常用的工具集合。IDE开发基于Solidity的智能合约,可以使用以下开发环境VSCode + Solidity Plugin Remix Solidity IDE (remix.ethereum.org) TruffleTruffle 是针对基于以太坊的 Solidity 语言的一套开发框架。本身基于 Javascript。内置的智能合约编译,链接,部署和二进制文件的管理。 快速开发下的自动合约测试。 脚本化的,可扩展的部署与发布框架。 部署到不管多少的公网或私网的网络环境管理功能 使用 EthPM&NPM 提供的包管理,使用 ERC190 标准。 与合约直接通信的直接交互控制台(写完合约就可以命令行里验证了)。 可配的构建流程,支持紧密集成。 在 Truffle 环境里支持执行外部的脚本。 安装 Trufflenpm install -g truffle 复制代码mkdir myproject && cd myproject && truffle init 复制代码GanacheGanache 是一个带有图形界面的本地运行的以太坊区块链浏览器/模拟器,它在本地运行了一个 RPC Server,通过连接这个 Ganache,我们可以完成智能合约的本地测试,而不需要真正的接入以太坊的公网或测试网络。通过使用 Ganache,你可以快速的看到你的应用是如何影响区块链的。其中细节:如你的账户、余额、合约及 Gas 成本。Gethgeth 的全称是 go-ethereum,是以太坊的官方钱包客户端。Geth 是基于命令行的。 通过使用 Geth 和相关参数,我们可以接入以太坊的公网,测试网以及私有网络。 以太坊除了主网络,还有各种各样的测试网络。使用 geth 前要先解决要进入哪一个网络。 Geth 相当于在本机启动一个以太坊网络节点,但是通过参数控制,可以让节点选择成为全节点或者轻节点。Geth 控制台提供 admin、debug、eth、miner、net、personal、rpc、txpool、web3 等服务及命令。比如有这些常用的操作:eth.blockNumber 可以查看当前的区块高度,总共有多少区块 eth.getBlock(xxx) 可以查看指定区块的信息 eth.accounts 查看当前钱包的账户地址,当第一次运行私有链网络的时候,没有账户,需要新建 eth.coinbase 矿工账户,当网络进行挖矿操作挖到新的区块后,奖励会到这个账户里 personal.newAccount() 新建账户,会提示输入密码,之后账户会以加密好的私钥文件存到data/keystore目录下 miner.start(threas_number) 开始挖矿,前提是当前钱包已经有coinbase矿工账户 miner.stop() 停止挖矿 图示在 geth 命令行下新建账户及获取余额操作:MistMist 是以太坊的官方图形钱包,通过该钱包,用户可以很方便的管理账户,查看余额,以及发送和接收交易。Mist 还有一个非常实用的功能就是,编译和部署 Solidity 智能合约。web3.jsweb3.js 提供了 web3 对象,封装了一组可以用来操作智能合约的方法。底层实现上,它通过 RPC 调用与本地节点 geth 进行通信。geth 本身就可以与合约进行交互,通过 web3.js 再封装了一层,这样我们可以使用 js 程序与合约交互,方便开发。引入npm install web3 复制代码以太坊通过 web3 的交互流程大致如下:总结随着区块链近些年的大红大紫,DApp 被推上了风口浪尖,本文从技术的角度大致介绍了一下 DApp 所涉及的技术要点。后续的文章,可以更加详细的分享一些 DApp 开发的具体案例,引入跟介绍一些 DApp Demo 跟具体开发流程等。文 / 李工普通程序猿,长期混迹移动互联网 曾供职 91 与百度,现任区块链开发工程师 资深韭菜,在韭菜经历中学习到扎实的区块链知识本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:knownsec-fed.com/2018-08-10-…想要看到更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,我们会尽可能回复。感谢您的阅读。
2023年03月24日
6 阅读
0 评论
0 点赞
1
...
77
78
79
...
109