美文网首页Dapp开发区块链研习社区块链
iOS 以太坊钱包 Trust源码解析

iOS 以太坊钱包 Trust源码解析

作者: MasonFu | 来源:发表于2018-03-27 15:53 被阅读9390次

从2017年下半年开始,区块链技术迅速火遍技术圈,投资圈,炒币圈...背景就不多说了,自行查阅。工作需要,近几个月一直在了解学习区块链相关的技术,也尝试着做出了一个基于以太坊的钱包APP,支持Token(ERC20)及DAPP。其实大部分技术基础都来源于github上的一些优秀的开源钱包代码。这篇文章就以 Trust Wallet 这个开源项目为例,描述一下典型的钱包应用都具备哪些功能和支撑的技术点,共同学习。

PS.区块链技术涉及到的知识点很庞杂,一些如区块链、以太坊、智能合约、钱包等基础概念就不细说了,感兴趣的可自行查阅

Trust Wallet

在众多开源的虚拟币钱包项目里,Trust做的算是非常完善和稳定的,代码风格、架构设计,技术栈都很新颖,并且已经在国外的AppStore上架,对应的Android版本也已开源。下面是Trust iOS版本的一些截图,四个Tab页分别为钱包、交易、Token、DAPP浏览器

  1. DAPP浏览器:基于Web浏览器,提供其支持的若干个交易网站,如加密猫游戏等,支持web在移动端进行本地钱包交易的功能
  2. 交易:展示当前钱包地址在以太坊中产生的交易记录,包括Token的交易记录,同时可收发以太币
  3. 通证:展示当前钱包地址在以太坊中,以及发生过交易的Token(ERC20)的余额状态;以太坊Token的转账功能
  4. 设置:切换钱包地址和目标网络,即连接的结点信息,Trust提供了若干主网及测试网选项;钱包地址的管理,例如创建、导入、备份、切换等
Trust

一、钱包(Wallet)

钱包应用的核心功能之一,就是地址的管理:生成,导入,导出和备份,以及余额查询等功能。在Trust中,主要由EtherKeystore模块封装了以上这些功能,结构如下:

wallet

以太坊,或者说区块链当中的唯一标识,就是地址Address,一个地址的产生,简单来说就是一对公钥私钥的产生,以太坊的交易地址为20-byte长的字节串,来源于私钥,通过SHA3 keccak256计算可得出一串40个字符长度的EIP55串,这个串就是我们经常看到的可对外公开的以太坊地址(例如0x5E9c27156a612a2D516C74c7a80af107856F8539

钱包的私钥相当于账户的户名及密码,私钥的处理和备份需要相当谨慎,一般常用的地址导入及备份方式有keystore、private key、mnemonic三种,不同的钱包偏重不同。Trust开发者单独封装了一个TrustKeystore的库,库中使用了很多加密算法相关的库,例如使用Apple的Security库创建密钥对,使用TrezorCrypto开源库的散列算法、助记词等算法处理私钥等。

Tip:如果在自己的项目中使用TrustKeystore,记得将其pod工程编译优化选项置为Fast,Whole Module Optimization [-O -whole-module-optimization],否则在密钥解析时会非常慢

令牌(ERC20 Token)

区块链的智能合约,可以理解为双方在区块链资产上交易转账时,触发执行的一段代码(合同),我们称它为智能合约。以太坊可以创建任何智能合约,这里的令牌就属于可以表示数字资产的智能合约,而这些数字资产被称为以太坊代币(Token)。Token有许多不同类型的标准,例如ERC20、ERC721、ERC223等。

ERC-20标准是在2015年11月份推出的,它定义了通用的Token代币所应支持的协议和接口标准,使基于以太坊众多的Token可以规范化,方便统一接入和操作。一个Token的基础属性有以下几点:

  • total supply //发行总量
  • name // 代币名称
  • decimal // 数量精度
  • symbol // 单位

Trust当中使用开源数据库Realm,对用户手动添加或者从交易记录中提取到的Token对象进行本地存储,如名称、合约地址、单位、精度,以及余额、价格等信息。 同时启动了Timer刷新Token的余额信息。

余额

以太坊余额与Token余额的查询分为两种途径:

  1. 以太坊:Trust中通过开源第三方库APIKit(网络请求封装库)以及JSONRPCKit(json RPC远程调用库)来进行以太坊节点的RPC接口调用。对应的余额查询接口为“eth_getBalance”。Trust调用的是Infura免费提供的以太坊主网及测试网节点。

Infura,以太坊开发服务平台,由于自建以太坊结点需要花比较多的时间和空间来同步区块,我们可以基于Infura可以简单很多,它免费提供公开以太坊节点和测试节点以调用,去官网只需要提供email,注册后就可得到专属的API地址。

  1. Token: Trust中对Token的余额查询方式为web3.js,在本地Native层开启一个不可见的wkwebview并且load进来index.html中的JS代码(创建web3对象、provider),通过JavaScriptKit开源库与webview配合完成以太坊智能合约接口的调用。ERC20代币合约的标准余额接口为“balanceOf”

web3.js是以太坊提供的一个Javascript库,它封装了以太坊的JSON RPC API,提供了一系列与区块链交互的Javascript对象和函数,包括查看网络状态,查看本地账户、查看交易和区块、发送交易、编译/部署智能合约、调用智能合约等,其中最重要的就是与智能合约交互的API。

结构图

token

二、转账(Transfer)

以太坊以及Token的转账流程相对比较复杂,主要涉及到gas费用的计算、合约调用,签名,提交交易信息。每笔交易首先需要调用“eth_estimateGas”接口获取本次交易的gas费用(用户可修改,理论上gas费越高,交易成交的速度就会越快),之后调用“eth_getTransactionCount”接口获取本次交易nonce值,通过钱包私钥对交易data进行签名,最后调用“eth_sendRawTransaction”接口发送交易信息,至此交易已经提交到以太坊结节,等待矿工执行

需要注意的是,对于Token的转账,交易的Data属性值可以看作Contract ABI的填充,这里来说就是ERC 20合约的Transfer标准方法,方法填参和编码后通过私钥签名放入transaction结构里发送给以太坊节点,在矿工成功挖矿后才会促使以太坊节点解析并执行其中的合约代码,完成Token的转账

transfer

三、交易(Transaction)

交易列表及明细是钱包应用不可或缺的一部分,由于以太坊API未提供交易列表的获取接口,Trust是通过第三方服务节点拉取的指定地址的交易列表,当然我们也可以通过etherscan.io平台的API进行交易列表信息的获取,经过简单的注册即可得到专属的API Key。

etherscan.io是 2015 年推出的一个以太坊区块探索和分析的分布式智能合同平台, 由于区块链中的交易信息等数据都是公开透明的 , Etherscan如同探索以太坊的窗口, 用户可以使用其查看自己的交易详情以及以太坊中的任何信息,开发者也可以调用其开发的API接口。

Trust将拉取到的交易信息通过Realm存储到本地数据库中,每次以分页形式拉取最新的交易信息,同时后台运行了一个刷新线程,通过“eth_getTransactionByHash”方法更新的交易状态

Transactions

四、分布式应用(DAPP)

DAPP在移动APP中的实现,简单来说,就是通过webview注入JS代码,在native端响应请求。Trust在WKWebView初始化时在WKWebViewConfiguration中加入WKUserScript自定义的JS代码以及JS代码中若干的响应方法(例如SignTransaction),native代码通过WKScriptMessageHandler协议响应网页中JS的调用,即完成了网页中点击购买,本地native代码完成支付的整个流程。

DAPP

开源库

在Trust Wallet这个开源的纯Swift的项目里,用到了20多个开源三方库,APP的组织架构为MVVM,内部以Coordinator为单位进行模块之间的协作和调用,下面是我从Trust内使用的开源库中抽出比较好玩的几个项目介绍下,其实每个库都可以写一篇文章了,我只简单介绍下,深入的话请自行查阅

APIKit

一个轻量的类型安全的网络请求库,两个主要特点,其一是简便快速的调用方式,其二是Request/Response的类型关联。以github的https://api.github.com/rate_limitAPI接口为例,使用APIKit调用的大致流程如下:

  1. 定义Request协议基类、根url访问地址

     protocol GitHubRequest: Request {
     }
         
     extension GitHubRequest {
         var baseURL: URL {
             return URL(string: "https://api.github.com")!
         }
     }
    
  2. 定义返回类型的Model 对象

     struct RateLimit {
         let count: Int
         let resetDate: Date
     
         init?(dictionary: [String: AnyObject]) {
             guard let count = dictionary["rate"]?["limit"] as? Int else {
                 return nil
             }
     
             guard let resetDateString = dictionary["rate"]?["reset"] as? TimeInterval else {
                 return nil
             }
     
             self.count = count
             self.resetDate = Date(timeIntervalSince1970: resetDateString)
         }
    

    }

  3. 定义具体的GetRateLimitRequest对象,实现Request协议要求的方法,例如具体的path、method、response从JSON到Model的转换等:

     struct GetRateLimitRequest: GitHubRequest {
         typealias Response = RateLimit
     
         var method: HTTPMethod {
             return .get
         }
     
         var path: String {
             return "/rate_limit"
         }
     
         func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
             guard let dictionary = object as? [String: AnyObject],
                   let rateLimit = RateLimit(dictionary: dictionary) else {
                 throw ResponseError.unexpectedObject(object)
             }
     
             return rateLimit
         }
     }
    
  4. 发送请求

     let request = GetRateLimitRequest()
     Session.send(request) { result in
         switch result {
         case .success(let rateLimit):
             print("count: \(rateLimit.count)")
             print("reset: \(rateLimit.resetDate)")
     
         case .failure(let error):
             print("error: \(error)")
         }
    

    }

是不是so easy?

Moya

Moya与APIKit有着类似的设计思想,都是将网络层相关的对象、参数或者数据抽象化,这样使得网络层结构更加清晰、接口之间更独立和规范,使用起来非常简单。但Moya要比APIKit更强大,它是基于Alamofire库在网络层上的完全封装,开发在应用层可以只单独依赖和调用Moya网络层即可。最大的特点是基于Moya可以很容易的构建出服务器接口组(API Service),独立性更高,方便维护、测试和移植。下面是官方文档给出的结构示意图:

Moya

再以github的https://api.github.com/rate_limitAPI接口为例,使用Moya调用的大致流程如下:

  1. 定义Service及其提供的所有接口

     enum GithubService {
         case rateLimit
     }
    
  2. 实现TargetType协议,明确API的调用path、parameters、method等

     // MARK: - TargetType Protocol Implementation
     extension GithubService: TargetType {
         var baseURL: URL { return URL(string: "https://api.github.com")! }
         var path: String {
             switch self {
             case . rateLimit:
                 return "/rate_limit"
             }
         }
         var method: Moya.Method {
             switch self {
             case .rateLimit:
                 return .get
             }
         }
         var task: Task {
             switch self {
             case . rateLimit: // Send no parameters
                 return .requestPlain
             }
         }
         var sampleData: Data {
             return Data()
         }
         var headers: [String: String]? {
             return ["Content-type": "application/json"]
         }
     }
    
  3. 创建Service及具体的Request,发送

     let provider = MoyaProvider<GithubService>()
     provider.request(.rateLimit) { result in
         switch result {
         case let .success(moyaResponse):
             let data = moyaResponse.data
             let statusCode = moyaResponse.statusCode
             // do something with the response data or statusCode
         case let .failure(error):
         }
     }
    

这里的provider就是抽象的Service接口组,可以按照项目的业务划分有一个或者多个,provider是更高层次的划分,APIKit只提供了一个全局的Session服务对象,在Request对象模型上进行了不同业务的划分,这是它们最大的区别之一。

R.swift

App开发项目中存在大量的资源文件或者对象,例如nib、storyboard、image、file、font、color、string等等。引用这些资源基本都是以字符串类型去载入,例如UIImage.init(named: "setting_icon"),如果对象名称改变或者输入有误都将无法正确载入资源,你只能万分小心的copy/paste这些资源名称。

R.swift就是为解决这个问题而生,具有强类型关联、编译错误检查、自动代码填充的功能,即安全又方便。基本原理就是在Xcode每次build期间自动读取解析工程目录内引用的资源文件以及创建的资源文件(例如TableViewCell.nib),将这些资源以代码的形式封装在一个动态生成的R.generated.swift的文件中。例如我在Assets.xcassets中添加了一个settings_icon图标,编译后R.generated.swift中将自动得到下面的代码段:

  /// This `R.image` struct is generated, and contains static references to 1 images.
  struct image {
    /// Image `setting_icon`.
    static let setting_icon = Rswift.ImageResource(bundle: R.hostingBundle, name: "setting_icon")
    
    /// `UIImage(named: "setting_icon", bundle: ..., traitCollection: ...)`
    static func setting_icon(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
      return UIKit.UIImage(resource: R.image.setting_icon, compatibleWith: traitCollection)
    }
    
    fileprivate init() {}
  }

这样,在我们需要使用资源时,不再是

let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let color = UIColor(named: "indictator highlight")
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")

而是

let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let color = R.color.indicatorHighlight()
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")

相关文章

网友评论

  • 040886faeec9:研究dapp浏览器的 加个好友 846966684 人多就建群
    林_柏显:大神.加了你的QQ
  • 廖丹_18be:关于token余额的查询有些疑问,Trust源码里貌似并没有用web3.js,跟踪代码发现走的还是RPC api的调用,eth_getBalance,求大神指点下
    廖丹_18be:@林_柏显 是的,难道认识?
    小朴同学:不用怀疑,就是走的RPC调用,他的源码已经封装了对合约函数的调用。作者研究是6月前,可能trust在这期间更新了。而且这种方式是以太坊官方推荐的
    林_柏显:你现在也在搞区块链了?
  • 8337ea5e8883:您好,看到您的文章质量非常高,很想邀请您成为虫洞社区的首批优质内容签约作者。虫洞社区是专业的区块链技术学习社区。虫洞社区鼓励内容生产者产生高质量内容,并给予合理的回报,也希望能帮助内容消费者获得高质量的区块链内容,并让数字货币投资者获得有价值的投资洞见。同时,虫洞社区已经积累了大量的区块链深度从业者,便于作者建立个人品牌。不知道是否方便加您微信细聊?
  • 梅某:有交流群么,有好些不明白的啊:sob:
    AdhereDamon:@梅某 加下我QQ 993757654! 谢谢 一起讨论下!
    梅某:@KWK 只是能运行了,还在研究中……
    AdhereDamon:你的解决了吗 ?
  • AdhereDamon:作者您好! 我现在编译也过了,一运行项目都是语法上的错误啊! 这是什么情况啊!
    牛大发了:我也是,你解决了吗?求教
  • Wtfg:Undefined symbols for architecture arm64:
    "_random32", referenced from:
    _generate_k_random in ecdsa.o
    _random_buffer in rand.o
    _random_uniform in rand.o
    _random_permute in rand.o
    ld: symbol(s) not found for architecture arm64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)

    pod库已经安装完毕,但是报以上错误,请问楼主或者其他兄弟遇到这个情况吗?
    Wtfg:成功编译的兄弟们,麻烦分享下心得:smile: :smile:
    Wtfg:@无双3 先把这个库屏蔽下,其他库有引用他。
    无双3:你好,你pod库中这个“TrezorCrypto”是怎么安装上的啊
  • 63312a4d3f28:楼主:有的版本号在 github 上没有。eg:podfile 里面1.7 ,github 上1.5,实际使用有影响么???
  • 农夫_三拳:楼主 我使用Trust 导出的Keystory 和 助记词 在导入imtoken 的时候,会生成不同的钱包地址, 你有尝试过 两个钱包相互导入的功能吗?
    06acbc880629:应该是m / purpose' / coin_type' / account' / change / address_index路径不一样,你看看imtoken和trust的标准
  • 1e52e2c4b4a5:最近在做Android WebView加载Fomo3D执行JS注入,实现Android Native代码支付的功能,是否有也在做这个功能的朋友呢?一起讨论
    040886faeec9:能加我qq吗 846966684
  • blus_lee:有没有技术讨论群吗?
  • blus_lee:下载的trust iOS的运行不了 少东西
    妖妖零幺幺:@blus_lee 我这边 # pod 'Fabric' # pod 'Crashlytics', '~> 3.10'这两个库需要翻墙。也可以禁掉不用。
    blus_lee:@天空之城_bdfa pod install 老是不成功呢,[!] Error installing QRCodeReaderViewController 本人菜鸟,跪求赐教。
    天空之城_bdfa:少了pod导入的三方库,要先用cocoapods下载库,且必须全部下载成功为止
  • bb9a8b42c97a:老哥请问trust如何调用智能合约?求翻牌,谢谢🤝
    廖丹_18be:请问调用智能合约研究明白了吗,现在看得懵逼阶段啊。。。貌似要做不少自己组装的事情,这里对RPC api封装的很有限呢,求加qq:271013527
  • Running__:最近正在搞数字钱包,看了这篇文章,学到蛮多,谢谢!有个问题需要请教,文中这段话有点不太明白:“需要注意的是,对于Token的转账,交易的Data属性值可以看作Contract ABI的填充,这里来说就是ERC 20合约的Transfer标准方法,方法填参和编码后通过私钥签名放入transaction结构里发送给以太坊节点,在矿工成功挖矿后才会促使以太坊节点解析并执行其中的合约代码,完成Token的转账”,方便结合Trust代码解析吗?万分感谢!
    yangye_2016:你好,我也在搞数字货币这块,能否加个好友一起探讨下
    a群:Tip:如果在自己的项目中使用TrustKeystore,记得将其pod工程编译优化选项置为Fast,Whole Module Optimization [-O -whole-module-optimization],否则在密钥解析时会非常慢 这个过程的确很慢,我把他放到项目里了 具体该怎么加快速度呢 费解求教
    bb9a8b42c97a:层主请问这块你弄明白了吗?真心求教,谢谢
  • 九龙:"记得将其pod工程编译优化选项置为Fast,Whole Module Optimization [-O -whole-module-optimization]"
    这个是怎么设置的求解答
    九龙:@a群 就是在 Build Setting -> Optimization Level
    a群:你有答案了吗 求教

本文标题:iOS 以太坊钱包 Trust源码解析

本文链接:https://www.haomeiwen.com/subject/vbkscftx.html