Dapp开发全球区块链应用Dapp

基于Stellar公链iOSDApp

2019-01-04  本文已影响7人  一只不靠谱的猿_
stellar.png

前段时间,项目需求基于Stellar公链发行的衍生链,需求iOS安卓端开发DAPP钱包配合公链run,项目基本属于破冰,国内完全没有任何资料去参考,一路走来都是新技术点,全凭google和自己慢慢填坑,完全从0开始,现在项目基本成熟已经进入测试阶段,所以有空余时间写篇文章为其他需要玩恒星公链的攻城狮提点建议,讲讲坑.

Stellar API 传送门: https://www.stellar.org/developers/reference/
Stellar Swift Sdk 传送门: https://github.com/Soneso/stellar-ios-mac-sdk

首先,项目采用oc,swift混编,嵌入了部分C语言和C++,
整体布局由于去中心化APP的特异性,基本都是采用回调方式创建事件、构造Operation和Trancations,
采用的随机方式从.English单词表生成的12位助记词,通过bip39共识算法生成随机公私钥,

1 -- 创建账户

根据单词表随机12位 Mnemonic助记词
// MARK: - 初始化sdk
 let sdk = StellarSDK.init(withHorizonUrl: "*****") //由于工作原因这里的Horizon暂时不能公开,为公链的Horizon地址
 let mnemonic = Wallet.generate12WordMnemonic()
BIP39

与处理钱包seed的原始二进制或十六进制表示形式相比,助记码或句子更适合于人类交互.这个句子可以写在纸上,也可以通过电话告诉对方.

(1)首先,生成ENT比特的初始熵entropy(如下面的例子00000000000000000000000000000000,16进制,熵长度为32*4=128).
(2)通过对初始熵entropy取SHA256散列来获得CS位(CS= 熵长度/32=4,取得到的SHA256散列的前CS位)校验和,然后将校验和附加到初始熵的末尾.
(3)接下来,(熵entropy+校验和)被分成以11位为一组(一共MS组),每个组编码对应一个0-2047的数字,该数字作为一个索引到wordlist,对应获得wordlist上相应索引的值.
(4)最后,我们将这些数字转换成单词,最终合在一起作为助记句.

助记词必须以32位的倍数选择熵值entropy.随着熵值的增加,句子长度增加,安全性提高.我们将初始熵长度称为ENT,ENT的允许大小是128-256位,目前我采用的是bip39的256位算法.

为了从助记符创建二进制种子,我们使用PBKDF2函数(密钥拉伸(Key stretching)函数),使用助记词(UTF-8 NFKD)作为密码,使用字符串"助记词"+密码(UTF-8 NFKD)作为salt.迭代计数设置为2048(即重复运算2048次).使用hma - sha512作为伪随机函数.派生键的长度是512位(= 64字节,即最后的seed的长度).

因为这里考虑到以后钱包要和ETH,BTC等钱包攀上关系,所以从开始就已经着手准备HD协议:
这个seed之后将被bip32或相似的方法使用来生成hd wallet,将助记句转换为二进制种子句与生成句子完全无关.这导致了相当简单的代码;句子结构没有限制,客户机可以自由地实现自己的单词列表,甚至可以实现整个句子生成器,这允许在单词列表中灵活地进行类型检测或其他目的.
虽然使用不是由“生成助记符”部分中描述的算法生成的助记符是可能的,但不建议这样做,软件必须使用wordlist计算助记符句子的校验和,并在其无效时发出警告.所描述的方法还提供了可信的可否认性,因为每个密码都生成一个有效的种子(从而产生一个hd wallet),但是只有正确的一个才能使所需的钱包可用.

// MARK: - 根据12词助记词,导入账户
let bip39SeedData = Mnemonic.createSeed(mnemonic: mnemonic)
let masterPrivateKey = Ed25519Derivation(seed: bip39SeedData)
let purpose = masterPrivateKey.derived(at: 44) //purpose,coinType,account为3次算法外位偏移量
let coinType = purpose.derived(at: 358)
let account = coinType.derived(at: 0)
let keyPair = try! KeyPair.init(seed: Seed(bytes: account.raw.bytes))
print("key pair - accountId: \(keyPair.accountId)")
print("key pair - secretSeed: \(keyPair.secretSeed!)")

2 -- 查询账户

这里不做过多的描述,因为准备大篇幅的内容留在之后Trancation和XDR信封签名的过程,所以直接展示封装核心代码

// MARK: - 查询账户
sdk.accounts.getAccountDetails(accountId: keyPair.accountId) { (response) -> (Void) in
    switch response {
    case .success(let accountDetails):

        for balance in accountDetails.balances {
            switch balance.assetType {
            case AssetTypeAsString.NATIVE:
                print("balance: \(balance.balance) XLM") //native币余额
            default:
                print("balance: \(balance.balance) \(balance.assetCode!) issuer: \(balance.assetIssuer!)") //其他衍生发行币
            }
        }
        for signer in accountDetails.signers {
            print("signer public key: \(signer.publicKey)")
        }

        print("sequence number: \(accountDetails.sequenceNumber)")
        print("auth required: \(accountDetails.flags.authRequired)")
        print("auth revocable: \(accountDetails.flags.authRevocable)")

        for (key, value) in accountDetails.data {
            print("data key: \(key) value: \(value.base64Decoded() ?? "")")
        }
    case .failure(let error):
        print(error.localizedDescription)
    }
}

3 -- 转账操作

这里就要详细讲一下Operations for Transaction,因为坑是真的很多,而且国内也没有像样子的详细介绍说明,由于去中心化的关系,基本一些逻辑上的操作全部要最小公链节点(DApp)来操作,这就造成了基本一个trancation当中必然要包含多个动作.
我们就以转账这个操作来说,需要有不低于3个步骤:

(1)确认sourceAccount源账户中,余额是否充足,拿到sourceAccountKeyPair用以在接下来创建paymentOperation,以确保我们有当前序列号,
(2)查询destinationAccount目标账户是否激活开户(因为由于节点数据库的特异性,不可能链上全部账户全部存入Horizon数据库),未开户激活的账户,公链只默认存在于最小非共识节点(DApp终端),
(3)通过转账币种那种当前币的Asset,一般本币为native,其他衍生发行币是ASSET_TYPE_CREDIT_ALPHANUM4以下发行的

通过ALPHANUM4衍生发行币拿到Asset对象的简单过程:

    // MARK: 通过coin名字拿到 asset
    func getCoinNameAsset(coinName:String) -> Asset {
        var coinTypeAsset = Asset(type: AssetType.ASSET_TYPE_NATIVE)
        if coinName == "coin1" {
            do {
                let timeIssuerKeyPair = try KeyPair(accountId: "币1的发行人Issue公钥地址")
                coinTypeAsset = Asset(type: AssetType.ASSET_TYPE_CREDIT_ALPHANUM4, code: coinName, issuer: timeIssuerKeyPair)
            }
            catch {
                // 错误
            }
        }
        else if coinName == "coin2" {
            do {
                let hourIssuerKeyPair = try KeyPair(accountId: "币2的发行人Issue公钥地址")
                coinTypeAsset = Asset(type: AssetType.ASSET_TYPE_CREDIT_ALPHANUM4, code: coinName, issuer: hourIssuerKeyPair)
            }
            catch {
                // 错误
            }
        }
        return coinTypeAsset!
    }

接下来走入转账Operation:

    // MARK: - 转账
    @objc func transactions(mySecretSeed: String, toAccountId: String, coinAmount: NSInteger, memoText: String, coinName: String) -> Void {
        /* 源帐户,自己的帐户 */
        let sourceAccountKeyPair = try! KeyPair(secretSeed:mySecretSeed)
        do {
            /* 目标帐户 */
            let destinationAccountKeyPair = try KeyPair(accountId: toAccountId)
            /* 获取帐户数据,以确保我们有当前序列号 */
            sdk.accounts.getAccountDetails(accountId: sourceAccountKeyPair.accountId) { (response) -> (Void) in
                switch response {
                case .success(let accountResponse):
                    do {
                        /* 建立支付操作 */
                        let paymentOperation = PaymentOperation(destination: destinationAccountKeyPair,
                                                                asset: self.getCoinNameAsset(coinName: coinName),
                                                                amount: Decimal(coinAmount))
                        /* 构建包含我们支付操作的事务(transaction) */
                        let transaction = try Transaction(sourceAccount: accountResponse,
                                                          operations: [paymentOperation],
                                                          memo: Memo.text(memoText),
                                                          timeBounds:nil)
                        /* 用秘钥给transaction签名 */
                        try transaction.sign(keyPair: sourceAccountKeyPair, network: Network.public)
                        /* 提交transaction */
                        try self.sdk.transactions.submitTransaction(transaction: transaction) { (response) -> (Void) in
                            switch response {
                            case .success(_):
                                //success
                            case .failure(let error):
                                StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-交易", horizonRequestError:error)
                            }
                        }
                    } catch {
                        //交易过程中,数据错误
                    }
                case .failure(let error):
                    StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-查询", horizonRequestError:error)
                }
            }
        }
        catch  {
            if (self.stellarErrorBlock != nil) {
                self.stellarErrorBlock!("格式错误")
            }
        }
    }

以上转账Operation有几个小细节处,Network.public为当前公链horizon地址的publicNet,如果这里还是使用原始SDK中的测试net,会根本run不通公链horizon:

/* 用秘钥给transaction签名 */
 try transaction.sign(keyPair: sourceAccountKeyPair, network: Network.public)

---------------------------------------------------------------------------------------
//  Network.swift
//  stellarsdk
public enum Network: String {
    case `public` = "your public network"
    case testnet = "Test SDF Network ; September 2015"
    var networkId: Data {
        get {
            return self.rawValue.sha256Hash
        }
    }
}

4 -- 新账户激活(createOperation)

// MARK: - 给新账户,激活账户(不低于100流明)
    @objc func createActiviteAccount(mySecretSeed: String, toAccountId: String, coinAmount: NSInteger, memoText: String) -> Void {
        /* 源帐户,自己的帐户 */
        let sourceAccountKeyPair = try! KeyPair(secretSeed:mySecretSeed)
        do {
            /* 目标帐户 */
            let destinationAccountKeyPair = try KeyPair(accountId: toAccountId)
            /* 获取帐户数据,以确保我们有当前序列号 */
            sdk.accounts.getAccountDetails(accountId: sourceAccountKeyPair.accountId) { (response) -> (Void) in
                switch response {
                case .success(let accountResponse):
                    do {
                        /* 建立激活操作 */
                        let createOpention = CreateAccountOperation(destination: destinationAccountKeyPair, startBalance: Decimal(coinAmount))//不低于100流明
                        /* 构建包含我们支付操作的事务(transaction) */
                        let transaction = try Transaction(sourceAccount: accountResponse,
                                                          operations: [createOpention],
                                                          memo: Memo.text(memoText),
                                                          timeBounds:nil)
                        /* 用秘钥给transaction签名 */
                        try transaction.sign(keyPair: sourceAccountKeyPair, network: Network.public)
                        /* 提交transaction */
                        try self.sdk.transactions.submitTransaction(transaction: transaction) { (response) -> (Void) in
                            switch response {
                            case .success(_):
                                //success
                            case .failure(let error):
                                StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-交易", horizonRequestError:error)
                            }
                        }
                    } catch {
                        //交易过程中,数据错误
                    }
                case .failure(let error):
                    StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-查询", horizonRequestError:error)
                }
            }
        }
        catch  {
            //格式错误
        }
    }

5 -- 建立信任线

建立信任线的操作,基本跟转账Operation中差距不大,只是在打包XDR信封的时候,需要装入信封的Operation转变为changeTrustOperation,其余包括Asset对象创建都是同理.
当中有一点需要注意

changeTrustOperation创建的时候要确认,当前币中在token地址上是否有余额,如果有余额会报错horizon信任线失败,只有在全部转出余额为0的时候才能转变信任线为NO,并且当你想到转换信任线为YES的时候,需要资产发行人Issee,并且需要一个已经持有该币种的最小子节点(DApp终端)给与你最低流明,并开启信任操作.

    // MARK: -  "1"->建立信任, "0"->取消信任
    @objc func changeTrustTimeHour(mySecretSeed:String, coinName: String, trust:String) -> Void {
        /* 源帐户,自己的帐户 */
        let sourceAccountKeyPair = try! KeyPair(secretSeed:mySecretSeed)
        /* 获取帐户数据,以确保我们有当前序列号 */
        sdk.accounts.getAccountDetails(accountId: sourceAccountKeyPair.accountId) { (response) -> (Void) in
            switch response {
            case .success(let accountResponse):
                do {
                    //Decimal()->建立信任, Decimal(0)->取消信任
                    let changeTrustTimeHourOperation = ChangeTrustOperation(asset: self.getCoinNameAsset(coinName: coinName), limit: (trust == "0" ? 0 : 100000000))
                    /* 构建包含我们支付操作的事务(transaction) */
                    let transaction = try Transaction(sourceAccount: accountResponse,
                                                      operations: [changeTrustTimeHourOperation],
                                                      memo: Memo.none,
                                                      timeBounds:nil)
                    /* 用秘钥给transaction签名 */
                    try transaction.sign(keyPair: sourceAccountKeyPair, network: Network.public)
                   /* 提交transaction */
                    try self.sdk.transactions.submitTransaction(transaction: transaction) { (response) -> (Void) in
                        switch response {
                        case .success(_):
                            print((trust == "1" ? "信任" : "取消信任") + "success")
                        case .failure(let error):
                            StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-交易", horizonRequestError:error)
                        }
                    }
                }
                catch {
                    // 信任错误
                }
            case .failure(let error):
                StellarSDKLog.printHorizonRequestErrorMessage(tag:"func-transactions-查询", horizonRequestError:error)
            }
        }
    }

6 -- 查询交易记录

 // MARK: - 查询交易记录
    @objc func requestPaymentsRecord(accountId:String, limit:Int) -> Void {
        sdk.payments.getPayments(forAccount: accountId, order:Order.ascending, limit:limit) { response in
            switch response {
            case .success(let paymentsResponse):
                for payment in paymentsResponse.records {
                   //响应操作
                }
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
    

7 -- SHA256加密算法

#import <CommonCrypto/CommonDigest.h>

- (NSString *)SHA256 {
    const char *s = [self cStringUsingEncoding:NSASCIIStringEncoding];
    NSData *keyData = [NSData dataWithBytes:s length:strlen(s)];
    uint8_t digest[CC_SHA256_DIGEST_LENGTH] = {0};
    CC_SHA256(keyData.bytes, (CC_LONG)keyData.length, digest);
    NSData *out = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
    NSString *hash = [out description];
    hash = [hash stringByReplacingOccurrencesOfString:@" " withString:@""];
    hash = [hash stringByReplacingOccurrencesOfString:@"<" withString:@""];
    hash = [hash stringByReplacingOccurrencesOfString:@">" withString:@""];
    return hash;
}

8 -- 划重点!!! AES256算法!

严格地说,AES和Rijndae并不完全一样(虽然在实际应用中二者可以互换),因为Rijndael加密法可以支持更大范围的区块和密钥长度:AES的区块长度固定为128位,密钥长度则可以是128,192或256位;而Rijndael使用的密钥和区块长度可以是32位的整数倍,以128位为下限,256位为上限.加密过程中使用的密钥是由Rijndael密钥生成方案产生.
大多数AES计算是在一个特别的有限域完成的.
不带模式和填充来获取AES算法的时候,其默认使用AES/ECB/PKCS5Padding(输入可以不是16字节,也不需要填充向量).

这里有一个巨坑!!:
安卓和ios一同开发的攻城狮们注意了,这里的AES算法涉及到偏移位padding5和padding7的区别时候,肯定会让你们束手无策,这里有一个矛盾点就是ios的系统库<CommonCrypto/CommonDigest.h>仅仅支持AES256的padding5算法,而安卓的系统库仅仅支持AES256的padding7算法,所以就会产生一个最大的矛盾点,如果按照各自平台的偏移位去加解密,那最后的结果会导致在各自平台内完全可以加解密成功,但是如果跨平台的话就是因为偏移位问题出现,加解密位数报错或者加解密直接失败,这里最后找到的解决办法是采用KDF算法,密码偏移轮询

CCKeyDerivationPBKDF(kCCPBKDF2,                // algorithm算法
                     password.UTF8String,      // password密码
                     password.length,          // passwordLength密码的长度
                     salt.bytes,               // salt内容
                     salt.length,              // saltLen长度
                     kCCPRFHmacAlgSHA1,        // PRF
                     10000,                    // rounds循环次数
                     derivedKey.mutableBytes,  // derivedKey
                     derivedKey.length);       // derivedKeyLen derive:出自
并且需要设置一个buff密码偏移
// 密码偏移
static Byte saltBuff[] = {0,1,2,3,4,5,6,7,8,9,0xA,0xB,0xC,0xD,0xE,0xF};
接下来说下aes加解密
// AES256加密
- (NSString *)aes256_encryptWithPassword:(NSString *)pasword aesIV:(NSString *)iv
{
    NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding];
    NSData *AESData = [self AES128operation:kCCEncrypt
                                       data:data
                                        key:pasword
                                         iv:iv];
    NSString *baseStr_GTM = [self encodeBase64Data:AESData];
    NSLog(@"加密 \n 密码:%@ \n iv:%@ \n 结果:%@", pasword, User.aes256_iv, baseStr_GTM);
    return baseStr_GTM;
}
// AES256解密
- (NSString *)aes256_decryptWithPassword:(NSString *)pasword
{
    NSData *baseData = [[NSData alloc]initWithBase64EncodedString:self options:0];
    NSData *AESData = [self AES128operation:kCCDecrypt
                                       data:baseData
                                        key:pasword
                                         iv:User.aes256_iv];
    NSString *decStr = [[NSString alloc] initWithData:AESData encoding:NSUTF8StringEncoding];
    NSLog(@"解密 \n 密码:%@ \n iv:%@ \n 结果:%@", pasword, User.aes256_iv, decStr);
    return decStr;
}
AES256算法核心
/**
 *  AES加解密算法
 *  @param operation kCCEncrypt(加密)kCCDecrypt(解密)
 *  @param data      待操作Data数据
 *  @param key       key
 *  @param iv        向量
 */
- (NSData *)AES128operation:(CCOperation)operation data:(NSData *)data key:(NSString *)key iv:(NSString *)iv {
    
    char keyPtr[kCCKeySizeAES256 + 1];  //kCCKeySizeAES128是加密位数 可以替换成256位的
    bzero(keyPtr, sizeof(keyPtr));    
    // IV
    char ivPtr[kCCBlockSizeAES128 + 1];
    bzero(ivPtr, sizeof(ivPtr));
    [iv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];
    
    size_t bufferSize = [data length] + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);
    size_t numBytesEncrypted = 0;
    // 设置加密参数
    /** 这里设置的参数ios默认为CBC加密方式,如果需要其他加密方式如ECB,在kCCOptionPKCS7Padding这个参数后边加上kCCOptionECBMode,即kCCOptionPKCS7Padding | kCCOptionECBMode,但是记得修改上边的偏移量,因为只有CBC模式有偏移量之说 */
    CCCryptorStatus cryptorStatus = CCCrypt(operation,
                                            kCCAlgorithmAES128,
                                            kCCOptionPKCS7Padding,
                                            [[NSString AESKeyForPassword:key] bytes],
                                            kCCKeySizeAES256,
                                            ivPtr,
                                            [data bytes],
                                            [data length],
                                            buffer,
                                            bufferSize,
                                            &numBytesEncrypted);
    if(cryptorStatus == kCCSuccess) {
        NSLog(@"Success");
        return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
    } else {
        NSLog(@"Error");
    }
    free(buffer);
    return nil;
}

还剩余一些挂单之类的Operation今天没做过多介绍,本文持续更新中...

上一篇 下一篇

猜你喜欢

热点阅读