Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IOS-7438 Filecoin #824

Merged
merged 9 commits into from
Sep 4, 2024
100 changes: 100 additions & 0 deletions BlockchainSdk.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// FilecoinExternalLinkProvider.swift
// BlockchainSdk
//
// Created by Aleksei Muraveinik on 22.08.24.
// Copyright © 2024 Tangem AG. All rights reserved.
//

import Foundation

struct FilecoinExternalLinkProvider: ExternalLinkProvider {
private let baseExplorerUrl = "https://filfox.info"

var testnetFaucetURL: URL? {
URL(string: "https://faucet.calibnet.chainsafe-fil.io")
}

func url(transaction hash: String) -> URL? {
URL(string: "\(baseExplorerUrl)/message/\(hash)")
}

func url(address: String, contractAddress: String?) -> URL? {
URL(string: "\(baseExplorerUrl)/address/\(address)")
}
}
15 changes: 15 additions & 0 deletions BlockchainSdk/Blockchains/Filecoin/FilecoinFeeParameters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// FilecoinFeeParameters.swift
// BlockchainSdk
//
// Created by Aleksei Muraveinik on 30.08.24.
// Copyright © 2024 Tangem AG. All rights reserved.
//

import BigInt

struct FilecoinFeeParameters: FeeParameters {
let gasLimit: Int64
let gasFeeCap: BigUInt
let gasPremium: BigUInt
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// FilecoinNetworkProvider.swift
// BlockchainSdk
//
// Created by Aleksei Muraveinik on 27.08.24.
// Copyright © 2024 Tangem AG. All rights reserved.
//

import Combine
import Foundation

final class FilecoinNetworkProvider: HostProvider {
var host: String {
node.url.absoluteString
}

private let node: NodeInfo
private let provider: NetworkProvider<FilecoinTarget>

init(
node: NodeInfo,
configuration: NetworkProviderConfiguration
) {
self.node = node
provider = NetworkProvider<FilecoinTarget>(configuration: configuration)
}

func getActorInfo(address: String) -> AnyPublisher<FilecoinResponse.GetActorInfo, Error> {
requestPublisher(for: .getActorInfo(address: address))
}

func getEstimateMessageGas(message: FilecoinMessage) -> AnyPublisher<FilecoinResponse.GetEstimateMessageGas, Error> {
requestPublisher(for: .getEstimateMessageGas(message: message))
}

func submitTransaction(signedMessage: FilecoinSignedMessage) -> AnyPublisher<FilecoinResponse.SubmitTransaction, Error> {
requestPublisher(for: .submitTransaction(signedMessage: signedMessage))
}

private func requestPublisher<T: Decodable>(for target: FilecoinTarget.FilecoinTargetType) -> AnyPublisher<T, Error> {
provider.requestPublisher(FilecoinTarget(node: node, target))
.filterSuccessfulStatusAndRedirectCodes()
.map(JSONRPC.Response<T, JSONRPC.APIError>.self)
.tryMap { try $0.result.get() }
.eraseToAnyPublisher()
}
}
102 changes: 102 additions & 0 deletions BlockchainSdk/Blockchains/Filecoin/FilecoinTransactionBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// FilecoinTransactionBuilder.swift
// BlockchainSdk
//
// Created by Aleksei Muraveinik on 29.08.24.
// Copyright © 2024 Tangem AG. All rights reserved.
//

import BigInt
import TangemSdk
import WalletCore

enum FilecoinTransactionBuilderError: Error {
case filecoinFeeParametersNotFound
case failedToConvertAmountToBigUInt
case failedToGetDataFromJSON
}

final class FilecoinTransactionBuilder {
private let decompressedPublicKey: Data

init(publicKey: Wallet.PublicKey) throws {
self.decompressedPublicKey = try Secp256k1Key(with: publicKey.blockchainKey).decompress()
}

func buildForSign(transaction: Transaction, nonce: UInt64) throws -> Data {
guard let feeParameters = transaction.fee.parameters as? FilecoinFeeParameters else {
throw FilecoinTransactionBuilderError.filecoinFeeParametersNotFound
}

let input = try makeSigningInput(transaction: transaction, nonce: nonce, feeParameters: feeParameters)
let txInputData = try input.serializedData()

let preImageHashes = TransactionCompiler.preImageHashes(coinType: .filecoin, txInputData: txInputData)
let preSigningOutput = try TxCompilerPreSigningOutput(serializedData: preImageHashes)

return preSigningOutput.dataHash
}

func buildForSend(
transaction: Transaction,
nonce: UInt64,
signatureInfo: SignatureInfo
) throws -> FilecoinSignedMessage {
guard let feeParameters = transaction.fee.parameters as? FilecoinFeeParameters else {
throw FilecoinTransactionBuilderError.filecoinFeeParametersNotFound
}

let unmarshalledSignature = try SignatureUtils.unmarshalledSignature(
from: signatureInfo.signature,
publicKey: decompressedPublicKey,
hash: signatureInfo.hash
)

let signatures = DataVector()
signatures.add(data: unmarshalledSignature)

let publicKeys = DataVector()
publicKeys.add(data: decompressedPublicKey)

let input = try makeSigningInput(transaction: transaction, nonce: nonce, feeParameters: feeParameters)
let txInputData = try input.serializedData()

let compiledWithSignatures = TransactionCompiler.compileWithSignatures(
coinType: .filecoin,
txInputData: txInputData,
signatures: signatures,
publicKeys: publicKeys
)

let signingOutput = try FilecoinSigningOutput(serializedData: compiledWithSignatures)

guard let jsonData = signingOutput.json.data(using: .utf8) else {
throw FilecoinTransactionBuilderError.failedToGetDataFromJSON
}

return try JSONDecoder().decode(FilecoinSignedMessage.self, from: jsonData)
}

private func makeSigningInput(
transaction: Transaction,
nonce: UInt64,
feeParameters: FilecoinFeeParameters
) throws -> FilecoinSigningInput {
guard let value = transaction.amount.bigUIntValue else {
throw FilecoinTransactionBuilderError.failedToConvertAmountToBigUInt
}

return FilecoinSigningInput.with { input in
input.to = transaction.destinationAddress
input.nonce = nonce

input.value = value.serialize()

input.gasLimit = feeParameters.gasLimit
input.gasFeeCap = feeParameters.gasFeeCap.serialize()
input.gasPremium = feeParameters.gasPremium.serialize()

input.publicKey = decompressedPublicKey
}
}
}
27 changes: 27 additions & 0 deletions BlockchainSdk/Blockchains/Filecoin/FilecoinWalletAssembly.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// FilecoinWalletAssembly.swift
// BlockchainSdk
//
// Created by Aleksei Muraveinik on 25.08.24.
// Copyright © 2024 Tangem AG. All rights reserved.
//

import Foundation

struct FilecoinWalletAssembly: WalletManagerAssembly {
func make(with input: WalletManagerAssemblyInput) throws -> WalletManager {
FilecoinWalletManager(
wallet: input.wallet,
networkService: FilecoinNetworkService(
providers: APIResolver(blockchain: input.blockchain, config: input.blockchainSdkConfig)
.resolveProviders(apiInfos: input.apiInfo) { nodeInfo, _ in
FilecoinNetworkProvider(
node: nodeInfo,
configuration: input.networkConfig
)
}
),
transactionBuilder: try FilecoinTransactionBuilder(publicKey: input.wallet.publicKey)
)
}
}
161 changes: 161 additions & 0 deletions BlockchainSdk/Blockchains/Filecoin/FilecoinWalletManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//
// FilecoinWalletManager.swift
// BlockchainSdk
//
// Created by Aleksei Muraveinik on 30.08.24.
// Copyright © 2024 Tangem AG. All rights reserved.
//

import BigInt
import Combine

class FilecoinWalletManager: BaseManager, WalletManager {
var currentHost: String {
networkService.host
}

var allowsFeeSelection: Bool {
false
}

private let networkService: FilecoinNetworkService
private let transactionBuilder: FilecoinTransactionBuilder

private var nonce: UInt64 = 0

init(
wallet: Wallet,
networkService: FilecoinNetworkService,
transactionBuilder: FilecoinTransactionBuilder
) {
self.networkService = networkService
self.transactionBuilder = transactionBuilder
super.init(wallet: wallet)
}

override func update(completion: @escaping (Result<Void, any Error>) -> Void) {
cancellable = networkService
.getAccountInfo(address: wallet.address)
.withWeakCaptureOf(self)
.sink(
receiveCompletion: { [weak self] result in
switch result {
case .failure(let error):
self?.wallet.clearAmounts()
completion(.failure(error))
case .finished:
completion(.success(()))
}
},
receiveValue: { walletManager, accountInfo in
if accountInfo.nonce != walletManager.nonce {
walletManager.wallet.clearPendingTransaction()
}

walletManager.wallet.add(
amount: Amount(
with: .filecoin,
type: .coin,
value: accountInfo.balance / walletManager.wallet.blockchain.decimalValue
)
)

walletManager.nonce = accountInfo.nonce
}
)
}

func getFee(amount: Amount, destination: String) -> AnyPublisher<[Fee], any Error> {
guard let bigUIntValue = amount.bigUIntValue else {
return .anyFail(error: WalletError.failedToGetFee)
}

return networkService
.getAccountInfo(address: wallet.address)
.map { [address = wallet.address] accountInfo in
FilecoinMessage(
from: address,
to: destination,
value: String(bigUIntValue, radix: 10),
nonce: accountInfo.nonce,
gasLimit: nil,
gasFeeCap: nil,
gasPremium: nil
)
}
.withWeakCaptureOf(networkService)
.flatMap { networkService, message in
networkService.getEstimateMessageGas(message: message)
}
.withWeakCaptureOf(self)
.tryMap { (walletManager: FilecoinWalletManager, gasInfo) -> [Fee] in
guard let gasFeeCapDecimal = Decimal(stringValue: gasInfo.gasFeeCap) else {
throw WalletError.failedToGetFee
}

let gasLimitDecimal = Decimal(gasInfo.gasLimit)

return [
Fee(
Amount(
with: .filecoin,
type: .coin,
value: gasLimitDecimal * gasFeeCapDecimal / walletManager.wallet.blockchain.decimalValue
),
parameters: FilecoinFeeParameters(
gasLimit: gasInfo.gasLimit,
gasFeeCap: BigUInt(stringLiteral: gasInfo.gasFeeCap),
gasPremium: BigUInt(stringLiteral: gasInfo.gasPremium)
)
)
]
}
.eraseToAnyPublisher()
}

func send(_ transaction: Transaction, signer: any TransactionSigner) -> AnyPublisher<TransactionSendResult, SendTxError> {
networkService
.getAccountInfo(address: wallet.address)
.withWeakCaptureOf(transactionBuilder)
.tryMap { transactionBuilder, accountInfo in
let hashToSign = try transactionBuilder.buildForSign(
transaction: transaction,
nonce: accountInfo.nonce
)
return (hashToSign, accountInfo.nonce)
}
.withWeakCaptureOf(self)
.flatMap { walletManager, args in
let (hashToSign, nonce) = args
return signer
.sign(hash: hashToSign, walletPublicKey: walletManager.wallet.publicKey)
.withWeakCaptureOf(walletManager)
.tryMap { walletManager, signature in
try walletManager.transactionBuilder.buildForSend(
transaction: transaction,
nonce: nonce,
signatureInfo: SignatureInfo(
signature: signature,
publicKey: walletManager.wallet.publicKey.blockchainKey,
hash: hashToSign
)
)
}
}
.withWeakCaptureOf(self)
.flatMap { walletManager, message in
walletManager.networkService
.submitTransaction(signedMessage: message)
.mapSendError(tx: try? JSONEncoder().encode(message).utf8String)
}
.withWeakCaptureOf(self)
.map { walletManager, txId in
let mapper = PendingTransactionRecordMapper()
let record = mapper.mapToPendingTransactionRecord(transaction: transaction, hash: txId)
walletManager.wallet.addPendingTransaction(record)
return TransactionSendResult(hash: txId)
}
.eraseSendError()
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// FilecoinAccountInfo.swift
// BlockchainSdk
//
// Created by Aleksei Muraveinik on 27.08.24.
// Copyright © 2024 Tangem AG. All rights reserved.
//

import Foundation

struct FilecoinAccountInfo {
let balance: Decimal
let nonce: UInt64
}
Loading
Loading