mirror of
https://github.com/luxfi/safe-ios.git
synced 2026-10-30 15:19:20 +00:00
411 lines
14 KiB
Swift
411 lines
14 KiB
Swift
//
|
|
// ClaimingAppControllerTests.swift
|
|
// MultisigIntegrationTests
|
|
//
|
|
// Created by Dmitry Bespalov on 22.08.22.
|
|
// Copyright © 2022 Gnosis Ltd. All rights reserved.
|
|
//
|
|
|
|
import XCTest
|
|
import Solidity
|
|
@testable import Multisig
|
|
|
|
class ClaimingAppControllerTests: XCTestCase {
|
|
var controller: ClaimingAppController!
|
|
|
|
override func setUpWithError() throws {
|
|
try super.setUpWithError()
|
|
controller = ClaimingAppController(chain: .goerliChain())
|
|
}
|
|
|
|
func test_isPaused() {
|
|
let exp = expectation(description: "wait")
|
|
|
|
_ = controller.isSafeTokenPaused { result in
|
|
do {
|
|
let isPaused = try result.get()
|
|
XCTAssertTrue(isPaused)
|
|
} catch {
|
|
XCTFail("Failed: \(error)")
|
|
}
|
|
exp.fulfill()
|
|
}
|
|
|
|
waitForExpectations(timeout: 10)
|
|
}
|
|
|
|
func test_getVesting() {
|
|
let exp = expectation(description: "wait")
|
|
|
|
_ = controller.vesting(
|
|
id: "0xb531d86add1d1e7febeb042d52d0eb0c49688ef4781dc8332bfab9a23c9307e7",
|
|
contract: controller.configuration.userAirdrop,
|
|
completion: { result in
|
|
do {
|
|
let vesting = try result.get()
|
|
// unredeemed vesting is all zeroes
|
|
XCTAssertEqual(vesting.amount, 0)
|
|
} catch {
|
|
XCTFail("Failed: \(error)")
|
|
}
|
|
exp.fulfill()
|
|
})
|
|
|
|
waitForExpectations(timeout: 10)
|
|
}
|
|
|
|
func test_isRedeemed() {
|
|
let exp = expectation(description: "wait")
|
|
|
|
_ = controller.isVestingRedeemed(
|
|
hash: "0xb531d86add1d1e7febeb042d52d0eb0c49688ef4781dc8332bfab9a23c9307e7",
|
|
contract: controller.configuration.userAirdrop,
|
|
completion: { result in
|
|
do {
|
|
let isRedeemed = try result.get()
|
|
XCTAssertFalse(isRedeemed)
|
|
} catch {
|
|
XCTFail("Failed: \(error)")
|
|
}
|
|
exp.fulfill()
|
|
})
|
|
|
|
waitForExpectations(timeout: 10)
|
|
}
|
|
|
|
func test_getDelegate() {
|
|
let exp = expectation(description: "wait")
|
|
|
|
_ = controller.delegate(
|
|
of: "0x25854e2a49a6cdaec7f0505b4179834509038549",
|
|
completion: { result in
|
|
do {
|
|
let delegate = try result.get()
|
|
XCTAssertEqual(delegate, 0)
|
|
} catch {
|
|
XCTFail("Failed: \(error)")
|
|
}
|
|
exp.fulfill()
|
|
})
|
|
|
|
waitForExpectations(timeout: 10)
|
|
}
|
|
|
|
func test_getAllocations() {
|
|
let exp = expectation(description: "wait")
|
|
|
|
_ = controller.allocations(
|
|
address: "0x1230B3d59858296A31053C1b8562Ecf89A2f888b",
|
|
completion: { result in
|
|
do {
|
|
let allocations = try result.get()
|
|
// NOTE: because of the error in the test data and because the vestings
|
|
// already deployed on rinkeby, we have 3 allocations for this test address
|
|
XCTAssertEqual(allocations.count, 3)
|
|
XCTAssertEqual(allocations.first?.amount, "5000110000000000000000")
|
|
} catch {
|
|
XCTFail("Failed: \(error)")
|
|
}
|
|
exp.fulfill()
|
|
})
|
|
|
|
waitForExpectations(timeout: 10)
|
|
}
|
|
|
|
// delegate != nil → setDelegate(delegate)
|
|
func test_claiming_setDelegate_address() {
|
|
let transactions = controller.claimingTransactions(
|
|
amount: 0,
|
|
beneficiary: .zero,
|
|
// given delegate is not nil
|
|
delegate: "0x728cafe9fB8CC2218Fb12a9A2D9335193caa07e0",
|
|
timestamp: 0,
|
|
allocations: [],
|
|
safeTokenPaused: false
|
|
)
|
|
|
|
guard let transaction = transactions.first else {
|
|
XCTFail("Expected to find a transaction")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(transaction.to.address, Address(controller.configuration.delegateRegistry))
|
|
XCTAssertEqual(
|
|
transaction.data,
|
|
DataString(hex:
|
|
"0x" +
|
|
// keccak(setDelegate(bytes32,address))[0..<4]
|
|
"bd86e508" +
|
|
// id: bytes32
|
|
// == "safe.eth" utf8 bytes padded to bytes32
|
|
"736166652e6574680000000000000000" +
|
|
"00000000000000000000000000000000" +
|
|
// delegate: address (encoded as uint160)
|
|
"000000000000000000000000728cafe9" +
|
|
"fb8cc2218fb12a9a2d9335193caa07e0"
|
|
)
|
|
)
|
|
}
|
|
|
|
// delegate == nil → no `setDelegate` transaction
|
|
func test_claiming_setDelegate_nil() {
|
|
let transactions = controller.claimingTransactions(
|
|
amount: 0,
|
|
beneficiary: .zero,
|
|
delegate: nil,
|
|
timestamp: 0,
|
|
allocations: [],
|
|
safeTokenPaused: false
|
|
)
|
|
XCTAssertTrue(transactions.isEmpty)
|
|
}
|
|
|
|
func test_claiming_endToEnd() throws {
|
|
continueAfterFailure = false
|
|
|
|
let networkTimeout: TimeInterval = 3600
|
|
|
|
// safe address
|
|
let safeAddress: Address = "0xEe6f78FeD18A20Af43d394f0F7dDc1aCf5d96d01"
|
|
// safe owner keys
|
|
let safeOwnerAddress: Address = "0x30846555A08e3e63897b063782b715e901FF57Be"
|
|
let safeOwnerKey = try PrivateKey(mnemonic: "series dad erosion illness attract shove matrix able trial turtle casino alcohol", pathIndex: 0)
|
|
|
|
let amountToClaim: Sol.UInt128 = 100
|
|
|
|
let chain = Chain.goerliChain()
|
|
|
|
// get safe info
|
|
let safe: Safe = Safe.create(address: safeAddress.checksummed, version: "1.3.0", name: "Claim Test Safe", chain: chain, selected: true, status: .deployed)
|
|
|
|
let expInfo = expectation(description: "SafeInfo")
|
|
_ = App.shared.clientGatewayService.asyncSafeInfo(safeAddress: safeAddress, chainId: chain.id!) { result in
|
|
do {
|
|
let safeInfo = try result.get()
|
|
safe.update(from: safeInfo)
|
|
} catch {
|
|
XCTFail("Failed to get safe info: \(error)")
|
|
}
|
|
expInfo.fulfill()
|
|
}
|
|
waitForExpectations(timeout: networkTimeout)
|
|
|
|
// get all vesting data
|
|
var claimData: ClaimingAppController.ClaimingData!
|
|
let expData = expectation(description: "ClaimData")
|
|
controller.asyncFetchData(account: safeAddress, timeout: networkTimeout) { result in
|
|
do {
|
|
claimData = try result.get()
|
|
} catch {
|
|
XCTFail("Failed to get claim data: \(error)")
|
|
}
|
|
expData.fulfill()
|
|
}
|
|
waitForExpectations(timeout: networkTimeout)
|
|
|
|
// create redeem & claiming transaction data
|
|
guard var transaction = controller.claimingTransaction(
|
|
safe: safe,
|
|
amount: amountToClaim,
|
|
delegate: try! claimData.delegate != Sol.Address(safeOwnerAddress.data32) ? safeOwnerAddress : nil,
|
|
data: claimData
|
|
) else {
|
|
XCTFail("Claiming transaction not created")
|
|
return
|
|
}
|
|
|
|
// estimate transaction
|
|
|
|
var estimation: SCGModels.TransactionEstimation!
|
|
let expEstimate = expectation(description: "Estimate")
|
|
_ = App.shared.clientGatewayService.asyncTransactionEstimation(
|
|
chainId: transaction.chainId!,
|
|
safeAddress: transaction.safe!.address,
|
|
to: transaction.to.address,
|
|
value: transaction.value.value,
|
|
data: transaction.data!.data,
|
|
operation: transaction.operation
|
|
) { result in
|
|
do {
|
|
estimation = try result.get()
|
|
} catch {
|
|
XCTFail("Can't estimate transaction: \(error)")
|
|
}
|
|
expEstimate.fulfill()
|
|
}
|
|
waitForExpectations(timeout: networkTimeout)
|
|
|
|
// uncomment for debugging, when estimation call above fails
|
|
// transaction.nonce = 2
|
|
transaction.nonce = estimation.recommendedNonce
|
|
transaction.updateSafeTxHash()
|
|
|
|
// sign transaction
|
|
let signature = try Wallet.shared.sign(transaction, key: safeOwnerKey)
|
|
|
|
print(signature.hexadecimal)
|
|
|
|
// submit transaction
|
|
var submittedTransaction: SCGModels.TransactionDetails!
|
|
let expSubmit = expectation(description: "Submit")
|
|
_ = App.shared.clientGatewayService.asyncProposeTransaction(
|
|
transaction: transaction,
|
|
sender: AddressString(safeOwnerAddress),
|
|
signature: signature.hexadecimal,
|
|
chainId: transaction.chainId!
|
|
) { result in
|
|
do {
|
|
submittedTransaction = try result.get()
|
|
} catch {
|
|
XCTFail("Failed to submit transaction: \(error)")
|
|
}
|
|
expSubmit.fulfill()
|
|
}
|
|
waitForExpectations(timeout: networkTimeout)
|
|
|
|
XCTAssertNotNil(submittedTransaction)
|
|
}
|
|
|
|
func test_claimingTransactions() throws {
|
|
continueAfterFailure = false
|
|
|
|
guard let fixture = try Fixture(filename: "claiming_test_fixtures.json") else {
|
|
XCTFail("fixture not found")
|
|
return
|
|
}
|
|
|
|
for testCase in fixture.testCases {
|
|
print("Test: \(testCase.name)")
|
|
|
|
let transactions = controller.claimingTransactions(
|
|
amount: Sol.UInt128(testCase.amount)!,
|
|
beneficiary: Address(testCase.beneficiary)!,
|
|
delegate: testCase.delegate.flatMap { Address($0) },
|
|
timestamp: testCase.timestamp,
|
|
allocations: testCase.allocationInput,
|
|
safeTokenPaused: testCase.safeTokenPaused)
|
|
|
|
XCTAssertEqual(transactions.count, testCase.expectedTransactions.count, testCase.name)
|
|
|
|
for (index, (tx, exp)) in zip(transactions, testCase.expectedTransactions).enumerated() {
|
|
let infoText = "\(testCase.name): tx[\(index)]"
|
|
XCTAssertEqual(tx.to, AddressString(exp.to)!, infoText)
|
|
XCTAssertEqual(tx.data, DataString(exp.txData), infoText)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Fixture: Codable {
|
|
var testCases: [TestCase]
|
|
|
|
// each struct needs a conversion to the model struct
|
|
|
|
struct TestCase: Codable {
|
|
var name: String
|
|
var amount: String
|
|
var beneficiary: String
|
|
var delegate: String?
|
|
var timestamp: TimeInterval
|
|
var allocations: [Allocation]
|
|
var vestings: [Vesting]
|
|
var safeTokenPaused: Bool
|
|
var expectedTransactions: [ExpectedTransaction]
|
|
|
|
var allocationInput: [(allocation: Multisig.Allocation, vesting: ClaimingAppController.Vesting)] {
|
|
let allocationData: [Multisig.Allocation] = allocations.map { json in
|
|
return Multisig.Allocation(
|
|
account: AddressString(json.account)!,
|
|
chainId: json.chainID,
|
|
contract: AddressString(json.contract)!,
|
|
vestingId: DataString(hex: json.vestingID),
|
|
durationWeeks: json.durationWeeks,
|
|
startDate: json.startDate,
|
|
amount: UInt256String(UInt256(json.amount)!),
|
|
curve: json.curve,
|
|
proof: json.proof.map { DataString(hex: $0) },
|
|
tag: json.tag
|
|
)
|
|
}
|
|
let vestingData: [ClaimingAppController.Vesting] = vestings.map { json in
|
|
return ClaimingAppController.Vesting(
|
|
account: Sol.Address(hex: json.account)!,
|
|
curveType: Sol.UInt8(json.curveType),
|
|
managed: Sol.Bool(storage: json.managed),
|
|
durationWeeks: Sol.UInt16(json.durationWeeks),
|
|
startDate: Sol.UInt64(json.startDate),
|
|
amount: Sol.UInt128(json.amount)!,
|
|
amountClaimed: Sol.UInt128(json.amountClaimed)!,
|
|
pausingDate: Sol.UInt64(json.pausingDate),
|
|
cancelled: Sol.Bool(storage: json.cancelled)
|
|
)
|
|
}
|
|
let result = zip(allocationData, vestingData).map { $0 }
|
|
return result
|
|
}
|
|
|
|
struct Allocation: Codable {
|
|
var account: String
|
|
var chainID: Int
|
|
var contract: String
|
|
var vestingID: String
|
|
var durationWeeks: Int
|
|
var startDate: UInt64
|
|
var amount: String
|
|
var curve: Int
|
|
var proof: [String]
|
|
var tag: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case account
|
|
case chainID = "chainId"
|
|
case contract
|
|
case vestingID = "vestingId"
|
|
case durationWeeks, startDate, amount, curve, proof, tag
|
|
}
|
|
}
|
|
|
|
struct ExpectedTransaction: Codable {
|
|
var to: String
|
|
var data: String
|
|
|
|
var txData: Data {
|
|
let lines = data.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "\n").map(String.init)
|
|
let bytes = lines.filter { !$0.hasPrefix("//") }
|
|
let hex = bytes.joined()
|
|
let result: Data = Data(hex: hex)
|
|
return result
|
|
}
|
|
}
|
|
|
|
struct Vesting: Codable {
|
|
var account: String
|
|
var curveType: Int
|
|
var managed: Bool
|
|
var durationWeeks: Int
|
|
var startDate: Int
|
|
var amount: String
|
|
var amountClaimed: String
|
|
var pausingDate: Int
|
|
var cancelled: Bool
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ClaimingAppControllerTests.Fixture {
|
|
init?(filename: String) throws {
|
|
let name = filename.split(separator: ".").map(String.init)
|
|
assert(name.count == 2)
|
|
// load data from bundle
|
|
guard let resource = Bundle(for: ClaimingAppControllerTests.self).url(forResource: name[0], withExtension: name[1]) else {
|
|
return nil
|
|
}
|
|
let data = try Data(contentsOf: resource)
|
|
self = try JSONDecoder().decode(Self.self, from: data)
|
|
for t in testCases {
|
|
assert(t.allocations.count == t.vestings.count, "allocation must match vesting")
|
|
}
|
|
}
|
|
}
|