# Learn Solidity Series 10: Make Some Dapps **Published by:** [bit1](https://paragraph.com/@bit1/) **Published on:** 2022-03-18 **URL:** https://paragraph.com/@bit1/learn-solidity-series-10-make-some-dapps ## Content 一、投注合约主要功能点:1)投注;2)开奖;3)退奖;4)获取奖池奖金;5)返回当前期数;6)返回中奖者地址;7)返回参与彩民的地址;1.1 定义合约属性合约主要包含有四个属性:contract Lottery { address manager; // 管理员 address[] players; // 投了注的彩民 address winner; // 上期彩票的胜出者 uint256 round = 1; // 第几期 } 管理员属性可以在创建合约的时候进行初始化。constructor() public { manager = msg.sender; } 1.2 投注假设每次投注只能投1个以太币。// 投注 function play() public payable { require(msg.value == 1 ether); players.push(msg.sender); } 1.3 开奖开奖就是在投注彩民数组players中随机选出一个彩民,然后将合约的余额转账到该彩民的地址上。 这里需要先定义一个修饰器,用于限定只有管理员角色有权执行开奖的方法。modifier onlyManager() { require(manager == msg.sender); _; } 下面是开奖方法的实现逻辑:function kaijiang() public payable { // 生成players数组的随机下标 bytes memory v1 = abi.encodePacked(block.difficulty, now, players.length); bytes32 v2 = keccak256(v1); uint v3 = uint256(v2) % players.length; // 获取中奖者 winner = players[v3]; // 把奖池的金额转账给中奖者 winner.transfer(address(this).balance); // 清空plays delete players; // 期数加1 round++; } 1.4 退奖只有管理员才可以发起退奖操作。// 退奖 function tuiJiang() public onlyManager { require(players.length != 0); // 把奖池的金额退还给每一个玩家 for (uint i = 0; i < players.length; i++) { players[i].transfer(1 ether); } // 清空plays delete players; // 期数加1 round++; } 值得注意的是,上面开奖和退奖方法中,都会执行transfer函数执行转账操作。上面代码会存在代码“重入”的风险。所以作为改善措施,应该按照Checks-Effects-Interactions模式编写函数代码。优化后的代码:function kaijiang() public payable { // 生成随机下标 bytes memory v1 = abi.encodePacked(block.difficulty, now, players.length); bytes32 v2 = keccak256(v1); uint v3 = uint256(v2) % players.length; // 获取中奖者 winner = players[v3]; // 清空plays delete players; // 期数加1 round++; // 把奖池的金额转账给中奖者 winner.transfer(address(this).balance); } // 退奖 function tuiJiang() public onlyManager { require(players.length != 0); // 清空plays delete players; // 期数加1 round++; // 把奖池的金额退还给每一个玩家 for (uint i = 0; i < players.length; i++) { players[i].transfer(1 ether); } } 上面代码将transfer操作放在方法最后执行。另外,上面代码通过delete players删除所有参与的玩家信息,这样会存在一定的风险:如果数组较大,可能会超过区块gas限制,从而引发out of gas异常。其实对于动态数组而言,使用length属性修改数组大小也可以达到同样效果。players.lenght = 0; 1.5 其他操作// 获取奖金池的金额 function getAmount() public view returns(uint256) { return address(this).balance; } // 获取管理员地址 function getManagerAddress() public view returns(address) { return manager; } // 返回当前期数 function getRound() public view returns(uint256) { return round; } // 返回中奖者地址 function getWinner() public view returns(address) { return winner; } // 返回参与彩民的地址 function getPlays() public view returns(address[]) { return players; } 二、众筹合约\ 主要功能点:1)创建众筹合约;2)获取所有众筹合约;3)获取发起者的众筹合约;4)获取参与者的众筹合约;5)参与众筹;6)获取已筹集到的金额;7)获取所有参与者;8)退款;9)众筹成功后,发起花费请求;10)审批花费请求;11)完成花费请求;12)获取众筹的剩余时间;13)获取参与者的数量;14)获取花费申请的详情; 2.1 创建众筹合约 第一步:定义合约。contract Funding { // 合约发起者 address public manager; // 众筹项目名称 string public projectName; // 目标金额 uint public targetMoney; // 支持金额 uint public supportMoney; // 项目结束时间,单位秒 uint public endTime; // 参与者 address payable[] investors; constructor(string memory _projectName, uint _targetMoney, uint _supportMoney, uint _duration, address _creator) public { manager = _creator; projectName = _projectName; targetMoney = _targetMoney; supportMoney = _supportMoney; endTime = now + _duration; } } 第二步:定义一个工厂合约,该合约保存了所有的众筹实例,并提供了操作众筹合约的方法; contract FundingFactory { // 平台管理员 address public platformManager; // 所有的众筹合约 address[] allFundings; // 自己创建的合约集合,key代表合约发起者地址,value代表合约机制的集合 mapping(address => address[]) creatorFundings; constructor() public { platformManager = msg.sender; } } 第三步:定义发起众筹的方法;function createFunding(string memory _projectName, uint _targetMoney, uint _supportMoney, uint _duration) public { Funding funding = new Funding(_projectName, _targetMoney, _supoortMoney, _duration, msg.sender); allFundings.push(address(funding)); creatorFundings[msg.sender].push(address(funding)); } 2.2 获取所有众筹合约和发起者的众筹合约接着在FundingFactory中添加其他操作合约的方法。// 获取所有众筹合约 function getAllFundings() public view returns(address[] memory) { return allFundings; } // 获取发起者的众筹合约 function getCreatorFundings() public view returns(address[] memory) { return creatorFundings[msg.sender]; } 2.3 获取参与者参与过的众筹集合第一步:为了便于维护参与者的众筹记录,我们定义一个合约,专门用来记录所有参与者参与过的众筹合约。contract SupportFunding { // 记录参与过的众筹 mapping(address => address[]) supportFundings; function setSupportFunding(address _supportor, address funding) public { supportFundings[_supportor].push(funding); } function getSupportFunding(address _supportor) public { return supportFundings[_supportor]; } } 第二步:在FundingFactory中增加supportFunding属性,并提供获取参与者参与过的合约方法; SupportFunding supportFunding; // 获取参与者参与过的众筹合约 function getSupportFunding() public view return(address[] memory) { return supportFunding.getFundings(msg.sender); } 2.4 参与众筹第一步:在Funding合约增加一个supportFunding属性,在该合约的构造函数中对该属性执行初始化;SupportFunding supportFunding; constructor(string memory _projectName, uint _targetMoney, uint _supportMoney, uint _duration, address _creator, SupportFunding _supportFunding) public { manager = _creator; projectName = _projectName; targetMoney = _targetMoney; supportMoney = _supportMoney; endTime = now + _duration; supportFunding = _supportFunding; } } 第二步:定义参与众筹方法;// 记录是否投资人,key代表参与者,value代表是否投资人 mapping(address => bool) isInvestors; // 参与众筹 function invest() public payable returns(uint) { // 约束条件:发送币的数量必须要等于支持金额 require(msg.value == supportMoney); // 记录投资人 investors.push(msg.sender); // 记录参与者为投资人 isInvestors[msg.sender] = true; // 记录参与者参与过的合约(当前合约) supportFunding.setFunding(msg.sender, address(this)); } 2.5 获取已筹集到的金额和所有的参与者 // 获取已经筹集到的金额,即当前合约的余额 function getBalance() public view returns(uint) { return address(this).balance; } // 获取所有的参与者 function getInvestors() public view returns(address payable[] memory) { return investors; } 2.6 退款如果合约还没结束,允许投资人退出众筹。所以我们在Funding合约中定义一个修饰器,该修饰器用于添加方法的约束条件。modifier onlyManager { require(msg.sender == manager); } 然后定义退款方法,并使用上面定义好的修饰器。// 退款 function refund() onlyManager public { // 循环遍历所有投资人,并向其转账 for (uint i = 0; i < investors.length; i++) { investors[i].transfer(supportMoney); } // 清空投资人数组 delete investors; } 2.7 申请花费如果众筹成功,管理员在使用众筹资金前,需要发起花费。 第一步:在Funding合约中定义一个结构体,用于记录花费的详情。// 花费申请状态,0代表申请中,1代表已批准,2代表已完成 enum RequestStatus { Voting, Approved, Completed } // 记录花费申请信息 struct Request { // 花费目的 string purpose; // 申请花费的金额 uint cost; // 转账给商家的地址 address payable seller; // 花费申请赞同的票数 uint approveCount; // 申请状态 RequestStatus status; // 参与者的投票状态,true代表已经投票,false代表还没投票 mapping(address => bool) hasVoted; } 第二步:在Funding合约中增加一个属性,该属性用于记录管理员发起的花费请求。 Request[] requests 第三步:定义申请花费的方法;// 发起花费申请 function createRequest(string memory _purpose, uint _cost, address payable _seller) onlyManager public { Request memory req = Request({ purpose: _purpose, cost: _cose * 10**18, // 将花费金额转换成单位wei seller: _seller, approveCount: 0, status: RequestStatus.Voting }); requests.push(req); } 2.8 发起申请审核审核通过条件:1)消息发起者是投资人;2)投资人之前没有投过票;// 对指定索引的申请进行审核 // 参数i代表数组的索引 function approveRequest(uint i) public { Request storage req = requests[i]; // 检查是否是投资人 require(isInvestors[msg.sender] == true); // 检查是否投过票 require(req.hasVoted[msg.sender] == false); // 赞同数增加 req.approveCount++; // 记录该投资人已经投过票 req.hasVoted[msg.sender] = true; // 如果票数过半,更新申请状态为Approved if (req.approveCount * 2 > investors.length) { req.status = RequestStatus.Approved; } } 2.9 完成花费结束花费申请的条件:1)合约余额必须要大于等于申请的花费金额;2)赞同票数要过半;// 完成花费 function finalizeRequest(uint i) onlyManager public { Request storage req = requests[i]; // 合约的余额必须要足够支付花费 require(address(this).balance >= req.cost); // 申请状态必须为已通过 require(req.status == RequestStatus.Approved); // 向商家转账 req.seller.transfer(req.cose); // 更新申请状态为已完成 req.status = RequestStatus.Completed; } 2.10 获取众筹的剩余时间、参与者的数量、花费申请的详情 function getLeftTime() public view returns(uint) { return endTime - now; } function getRequestCount() public view returns(uint) { return requests.length; } function getRequest(uint i) public view returns(string memory, uint, address, uint, RequestStatus) { Request storage req = requests[i]; return(req.purpose, req.cost, req.seller, req.approveCount, req.status); } 到这里为止,整个众筹合约的核心代码已经完成。三、智能评分合约该合约实现了根据每一个学员的计算分数指标m和n计算出他们的最终成绩。 首先在合约中定义一个结构体,用于封装学员相关的信息。contract SmartScore { struct Score { // 学生ID uint uid; // 课程ID uint cid; // m为被其他学生发现并成功评价的实验操作错误数量 uint m; // n为成功评价其他学生实验操作错误数量 uint n; // 分数 uint score; } } 定义一个计算学员成绩的函数。函数入参是一个数组类型,而数组中每一个元素也是一个长度为4的一维数组,分别用于存储学员ID,课程ID,分数指标m,分数指标n、以及最终成绩。// 设置完成后,计算学生成绩 function calc(uint[4][] memory data) public pure returns(string memory) { Score[] memory scores = new ScoreUnsupported embed; uint i = 0; for (i = 0; i < data.length; i++) { uint uid = data[i][0]; uint cid = data[i][1]; uint m = data[i][2]; uint n = data[i][3]; Score memory score = Score(uid, cid, m, n, 0); scores[i] = score; } // 找出最高分数,以n-m的值作为参考值 Score memory maxScore = scores[0]; for (i = 1; i < scores.length; i++) { int a1 = int(maxScore.n - maxScore.m); int a2 = int(scores[i].n - scores[i].m); if(a2 > a1) { maxScore = scores[i]; } } // 计算alpha参数的公式为:100 = 80 + (n - m) * alpha uint alpha = 20 / (maxScore.n - maxScore.m); // 计算每个学生的成绩 for (i = 0; i < scores.length; i++ ){ scores[i].score = 80 + (scores[i].n - scores[i].m) * alpha; } // 返回学员的成绩,返回格式: uid_cid_score#uid_cid_score#uid_cid_score#... string memory result = ""; for (i = 0; i < scores.length; i++ ){ Score memory sc = scores[i]; result = result.concat(uint2str(sc.uid).concat("_").concat(uint2str(sc.cid)).concat("_").concat(uint2str(sc.score))); if (i != scores.length - 1) { result = result.concat("#"); } } return result; } 因为solidity不支持uint和string类型之间的强制类型转换,因此这里我们自定义了一个uint2str函数,用于将一个uint类型变量转换成string类型变量。 uint2str函数的实现如下:function uint2str(uint i) internal pure returns (string memory c) { if (i == 0) { return "0"; } uint j = i; uint length; while (j != 0){ length++; j /= 10; } bytes memory b = new bytes(length); uint k = length - 1; while (i != 0){ b[k--] = byte(uint8(48 + i % 10)); i /= 10; } return string(b); } 上面代码就是将uint类型的变量中每一个数字转换成对应的ascii码后,再封装到bytes里面,最终再将bytes转换成string类型。 另外,合约里面还需要实现字符串的拼接。遗憾的是,solidity并没有提供字符串拼接的工具。所以需要我们自己来实现字符串的拼接。library StringUtils { function concat(string memory self, string memory s) internal pure returns (string memory) { bytes memory b = new bytes(_a.length + _b.length); bytes memory _a = bytes(self); bytes memory _b = bytes(s); uint k = 0; uint i = 0; for (i = 0; i < _a.length; i++) { b[k++] = _a[i]; } for (i = 0; i < _b.length; i++) { b[k++] = _b[i]; } return string(b); } } 这里我们定义了一个库合约,用于提供字符串拼接的函数。然后在SmartScore合约中通过using..for语法将库合约引入进来。 using StringUtils for string; 四、拍卖合约主要功能点:1)卖方发布商品;2)读取商品信息;3)投标;4)揭标;5)仲裁者确定中标者;6)获取赢家信息;7)获取参与竞标的人数;8)管理投标合约资金的发放;发布商品者(卖方):发布商品;投标者或出价者(买方):进行投标和揭标操作;\ 仲裁者:负责确定中标者;4.1 定义合约// 竞拍合约 contract Auction { // 用于统计竞标商品数量,作为ID uint public productIndex; // 该mapping存储了商品Id与竞标获得者地址的对应关系 mapping(uint => address payable) productIdInStore; // 该mapping存储了竞标获得者参与过的拍卖商品之间的关系 mapping(address => mapping(uint => Product)) stores; // 竞标商品的状态,0代表开始拍卖,1代表交易成功,2代表交易不成功 enum ProductStatus { Open, Sold, Unsold } // 竞标商品的使用状态,0代表未使用过,1代表使用过 enum ProductCondition { New, Used } // 竞标人信息 struct Bid { // 投标人 address bidder; // 竞标商品ID uint productId; // 虚拟投标价格 uint value; //是否已经揭标 bool revealed; } // 商品信息 struct Product { // 商品id uint id; // 商品名称 string name; // 商品类别 string category ; // 图片Hash string imageLink ; // 图片描述信息的Hash string descLink; // 竞标开始时间 uint auctionStartTime; // 竞标结束时间 uint auctionEndTime; // 竞标初始价格 uint startPrice; // 出价最高者 address payable highestBidder; // 赢家得标的价格 uint highestBid ; // 竞标价格第二名 uint secondHighestBid ; // 竞标总人数 uint totalBids ; // 竞标商品的状态 ProductStatus status; // 竞标商品的新旧标识 ProductCondition condition; // 存储所有竞标人的信息 mapping(address => mapping(bytes => Bid)) bids; } } 4.2 发布商品// 发布商品 function addProductToStore(string memory _name, string memory _category, string memory _imageLink, string memory _descLink, uint _auctionStartTime, uint _auctionEndTime ,uint _startPrice, uint _productCondition) public { // 开始时间需要小于结束时间 require(_auctionStartTime < _auctionEndTime, "开始时间不能晚于结束时间"); // 商品ID自增 productIndex += 1; // 创建Product实例 Product memory product = Product(productIndex,_name,_category,_imageLink,_descLink,_auctionStartTime,_auctionEndTime,_startPrice,address(0x0),0,0,0,ProductStatus.Open,ProductCondition(_productCondition)); // 保存商品 stores[msg.sender][productIndex] = product; productIdInStore[productIndex] = msg.sender; } 4.3 读取商品信息// 通过商品ID读取商品信息 function getProduct(uint _productId) public view returns (uint,string memory, string memory,string memory,string memory,uint ,uint,uint, ProductStatus, ProductCondition) { Product memory product = stores[productIdInStore[_productId]][_productId]; return (product.id, product.name,product.category,product.imageLink,product.descLink,product.auctionStartTime,product.auctionEndTime,product.startPrice,product.status,product.condition); } 4.4 投标投标必须要满足以下条件:\ 1)必须在竞拍时间内;\ 2)投标价格必须大于等于商品的拍卖价格;\ 3)投标人没有该商品的投标记录;// 投标, 其中_bid参数代表投标人的ID,由外部生成传入 function bid(uint _productId, bytes memory _bid) payable public returns (bool) { Product storage product = stores[productIdInStore[_productId]][_productId]; require(now >= product.auctionStartTime, "商品竞拍时间未到,暂未开始,请等待..."); require(now <= product.auctionEndTime,"商品竞拍已经结束"); require(msg.value >= product.startPrice,"设置的虚拟价格不能低于开标价格"); require(product.bids[msg.sender][_bid].bidder == address (0x0), "bidder的值必须为空"); // 设置投标人 product.bids[msg.sender][_bid] = Bid(msg.sender, _productId, msg.value, false); // 投标人数递增 product.totalBids += 1; // 返回投标成功 return true; } 4.5 揭标竞标结束后,由买方公告价格。最后以买方公告价格的最高者作为赢家。// 揭标(买方公告价格) function revealBid(uint _productId, string memory _amount, bytes memory _bid) public { // 通过商品ID获取商品信息 Product storage product = stores[productIdInStore[_productId]][_productId]; // 检查条件:投标必须要结束了 require(now > product.auctionEndTime,"竞标尚未结束,未到公告价格时间"); // 获取投标人信息 Bid memory bidInfo = product.bids[msg.sender][_bid]; // 检查条件:投标人不为空 require(bidInfo.bidder > address (0x0), "钱包地址不存在"); // 检查条件:必须是未揭标状态 require(bidInfo.revealed == false, "已经揭标"); // 退款金额 uint refund; // 公告价格 uint amount = stringToUint(_amount); // 如果买方设置的虚拟价格低于公告价格,则执行退款操作 if (bidInfo.value < amount) { refund = bidInfo.value; } else { // 如果第一个参与公告价格者,那么更新商品竞标信息,并将虚拟价格与实际竞标价格之间的差额退还给买方 if (address(product.highestBidder) == address (0x0)) { // 设置当前出价者为最高价的竞拍者 product.highestBidder = msg.sender; // 将公告价格作为最高价格 product.highestBid = amount; // 将商品的起始拍卖价格作为第二高价格 product.secondHighestBid = product.startPrice; // 将虚拟价格与公告价格之间的差额作为退款 refund = bidInfo.value - amount; } else { // 如果参与者不是第一个揭标,那么分为三种情况: // 第一种情况:实际竞标价大于商品的最高竞标价; // 第二种情况:实际竞标价小于商品的最高竞标价,但是大于第二高竞标价; // 第三种情况:实际竞标价小于第二高竞标价; if (amount > product.highestBid) { // 将原来的最高价竞拍者修改为第二高价竞拍者 product.secondHighestBid = product.highestBid; // 将原来最高竞拍价退还给最高价竞拍者 product.highestBidder.transfer(product.highestBid); // 将当前出价者作为最高价竞拍者 product.highestBidder = msg.sender; // 将当前出价作为最高价 product.highestBid = amount; // 将虚拟价格与公告价格之间的差额作为退款 refund = bidInfo.value - amount; } else if (amount > product.secondHighestBid) { product.secondHighestBid = amount; // 退还所有竞标款 refund = amount; } else { // 如果出价比第二高价还低的话,直接退还竞标款 refund = amount; } } // 更新状态为已揭标 product.bids[msg.sender][_bid].revealed = true; // 退款 if (refund > 0){ msg.sender.transfer(refund); } } } 下面是stringToUint函数的实现:function stringToUint(string memory s) pure private returns (uint) { bytes memory b = bytes(s); uint result = 0 ; for (uint i = 0; i < b.length; i++ ){ if (uint(uint8(b[i])) >= 48 && uint(uint8(b[i])) <= 57){ result = result * 10 + (uint(uint8(b[i])) - 48); } } return result; } 4.6 确定中标者仲裁者可以确定最终的中标方。function finalizeAuction(uint _productId) public { Product memory product = stores[productIdInStore[_productId]][_productId]; require(now > product.auctionEndTime, "当前时间必须大于竞拍结束时间"); require(product.status == ProductStatus.Open, "竞拍状态必须是开始状态"); require(product.highestBidder != msg.sender, "仲裁者不能够是最高出价者"); require(productIdInStore[_productId] != msg.sender, "仲裁者不能够是卖方"); // 如果竞拍人数为0,则无需做任何操作 if (product.totalBids == 0) { product.status = ProductStatus.Unsold; } else { // 创建托管合约实例 Escrow escrow = (new Escrow).value(product.secondHighestBid)(_productId, product.highestBidder, productIdInStore[_productId], msg.sender); // 竞拍合约包含了托管合约的引用 productEscrow[_productId] = address(escrow); // 更新状态 product.status = ProductStatus.Sold; // 中标方只需要支付第二高投标价格,差额会退还给中标方 uint refund = product.highestBid - product.secondHighestBid; // 退还差额 product.highestBidder.transfer(refund); } // 更新商品信息 stores[productIdInStore[_productId]][_productId] = product; } 4.7 管理资金发放上面代码创建了一个Escrow合约实例,该合约主要负责管理合约资金的发放。contract Escrow { // 商品ID uint public productId; // 买房(中标者) address payable public buyer; // 卖方 address payable public seller; // 仲裁者 address public arbiter; // 投标金额 uint public amount; // 是否已经将投标金额支付给卖方或买方 bool public isDisbursed; // 是否同意支付给卖方 mapping(address => bool) isReleased; // 同意支付人数 uint public releaseCount; // 是否同意退还给买方 mapping(address => bool) isRefunded; // 同意退还人数 uint public refundCount; event CreateEscrow(uint _productId, address _buyer, address _seller, address _arbiter); event UnlockAmount(uint _productId, string _operation, address _operator); event DisburseAmount(uint _productId, uint _amount, address _beneficiary); constructor(uint _productId, address _buyer, address _seller, address _arbiter) payable public { productId = _productId; buyer = _buyer; seller = _seller; arbiter = _arbiter; amount = msg.value; isRefunded= false; emit CreateEscrow(_productId, _buyer, _seller, _arbiter); } // 获取Escrow合约的详情 function getEscrowInfo() public view returns(address, address, address, bool, uint, uint) { return(buyer, seller, arbiter, isDisbursed, releaseCount, refundCount); } // 只要有两位任意买方、卖方或仲裁者同意,那么就会向卖方发放投标金额 function releaseToSeller(address caller) public { require(!isDisbursed, "支付状态必须是未支付"); if ((caller == buyer || caller == seller || caller == arbiter) && !isReleased[caller]) { isReleased[caller] = true; releaseCount += 1; emit UnlockAmount(productId, "release", caller); } if (releaseCount == 2) { seller.transfer(amount); isDisbursed = true; emit DisburseAmount(productId, amount, seller); } } // 只要有两位任意买方、卖方或仲裁者同意,那么就会向买方退还投标金额 function refundToBuyer(address caller) public { require(!isDisbursed, "支付状态必须是未支付"); if ((caller == buyer || caller == seller || caller == arbiter) && !isFunded[caller]) { isRefunded[caller] = true; refundCount += 1; emit UnlockAmount(productId, "refund", caller); } if (releaseCount == 2) { buyer.transfer(amount); isDisbursed = true; emit DisburseAmount(productId, amount, buyer); } } } 4.8 获取竞标赢家信息// 获取竞标赢家信息 function getWinnerInfo(uint _productId) public view returns (address, uint ,uint) { Product memory product = stores[productIdInStore[_productId]][_productId]; return (product.highestBidder, product.highestBid, product.secondHighestBid); } // 获取竞拍人数 function getTotalBids(uint _productId) public view returns(uint) { Product memory product = stores[productIdInStore[_productId]][_productId]; return (product.highestBidder, product.highestBid, product.secondHighestBid); } 上面就是竞标合约的所有代码介绍。 ## Publication Information - [bit1](https://paragraph.com/@bit1/): Publication homepage - [All Posts](https://paragraph.com/@bit1/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@bit1): Subscribe to updates