基于Stellar公鏈iOSDApp

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)

這里的createOperation有一些簡單說明,激活才做需要sourceAccount為當前已經激活的account,而且必須最少有公鏈設置的賬戶創建最低流明作為初始幣持有(如果低于初始幣持有,會扣除手續費,并且賬戶激活失敗),很坑...

// 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今天沒做過多介紹,本文持續更新中...

2018幣圈跌宕起伏,熊市太多,希望2019大家一起賺到缽盂滿盆一夜暴富...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容