diff --git a/Sources/ImmutableXCore/ImmutableX.swift b/Sources/ImmutableXCore/ImmutableX.swift index a0219bf..097b8bb 100644 --- a/Sources/ImmutableXCore/ImmutableX.swift +++ b/Sources/ImmutableXCore/ImmutableX.swift @@ -174,8 +174,12 @@ public struct ImmutableX { starkSigner: StarkSigner ) async throws -> CreateTransferResponse { try await transferWorkflow.transfer( - token: token, - recipientAddress: recipientAddress, + transfers: [ + .init( + token: token, + recipientAddress: recipientAddress + ), + ], signer: signer, starkSigner: starkSigner ) diff --git a/Sources/ImmutableXCore/Workflows/TransferWorkflow.swift b/Sources/ImmutableXCore/Workflows/TransferWorkflow.swift index 63864ed..1c43bcb 100644 --- a/Sources/ImmutableXCore/Workflows/TransferWorkflow.swift +++ b/Sources/ImmutableXCore/Workflows/TransferWorkflow.swift @@ -1,18 +1,27 @@ import Foundation +public struct TransferData { + public let token: AssetModel + public let recipientAddress: String + + public init(token: AssetModel, recipientAddress: String) { + self.token = token + self.recipientAddress = recipientAddress + } +} + class TransferWorkflow { /// This is a utility function that will chain the necessary calls to transfer a token. /// /// - Parameters: - /// - token: to be transferred (ETH, ERC20, or ERC721) - /// - recipientAddress: of the wallet that will receive the token + /// - transfers: contains a list of ``TransferData`` with `token` to be transferred (ETH, ERC20, or ERC721) + /// and `recipientAddress` of the wallet that will receive the token /// - signer: represents the users L1 wallet to get the address /// - starkSigner: represents the users L2 wallet used to sign and verify the L2 transaction /// - Returns: ``CreateTransferResponse`` that will provide the transfer id if successful. /// - Throws: A variation of ``ImmutableXError`` class func transfer( - token: AssetModel, - recipientAddress: String, + transfers: [TransferData], signer: Signer, starkSigner: StarkSigner, transfersAPI: TransfersAPI.Type = TransfersAPI.self @@ -20,46 +29,61 @@ class TransferWorkflow { let address = try await signer.getAddress() let response = try await getSignableTransfer( address: address, - token: token, - recipientAddress: recipientAddress, + transfers: transfers, api: transfersAPI ) - let signableResponse = try response - .signableResponses - .first - .orThrow(.invalidRequest(reason: "Invalid signable response")) - let starkSignature = try await starkSigner.signMessage(signableResponse.payloadHash) + + guard !response.signableResponses.isEmpty else { + throw ImmutableXError.invalidRequest(reason: "Invalid signable response") + } + + let starkSignatures = try await getStarkSignatures( + response.signableResponses, + starkSigner: starkSigner + ) let ethSignature = try await signer.signMessage(response.signableMessage) let signatures = WorkflowSignatures( ethAddress: address, ethSignature: ethSignature, - starkSignature: starkSignature + starkSignatures: starkSignatures ) return try await createTransfer( response: response, - signableResponse: signableResponse, signatures: signatures, api: transfersAPI ) } + private static func getStarkSignatures( + _ signableResponses: [SignableTransferResponseDetails], + starkSigner: StarkSigner + ) async throws -> [String] { + var signatures = [String]() + + for response in signableResponses { + let signature = try await starkSigner.signMessage(response.payloadHash) + signatures.append(signature) + } + + return signatures + } + private static func getSignableTransfer( address: String, - token: AssetModel, - recipientAddress: String, + transfers: [TransferData], api: TransfersAPI.Type ) async throws -> GetSignableTransferResponse { try await APIErrorMapper.map(caller: "Signable transfer") { try await api.getSignableTransfer( getSignableTransferRequestV2: GetSignableTransferRequest( senderEtherKey: address, - signableRequests: [ + signableRequests: transfers.map { SignableTransferDetails( - amount: token.quantity, - receiver: recipientAddress, - token: token.asSignableToken() - ), - ] + amount: $0.token.quantity, + receiver: $0.recipientAddress, + token: $0.token.asSignableToken() + ) + } ) ) } @@ -67,7 +91,6 @@ class TransferWorkflow { private static func createTransfer( response: GetSignableTransferResponse, - signableResponse: SignableTransferResponseDetails, signatures: WorkflowSignatures, api: TransfersAPI.Type ) async throws -> CreateTransferResponse { @@ -76,7 +99,7 @@ class TransferWorkflow { xImxEthAddress: signatures.ethAddress, xImxEthSignature: signatures.serializedEthSignature, createTransferRequestV2: CreateTransferRequest( - requests: [ + requests: response.signableResponses.enumerated().map { index, signableResponse in TransferRequest( amount: signableResponse.amount, assetId: signableResponse.assetId, @@ -85,9 +108,9 @@ class TransferWorkflow { receiverStarkKey: signableResponse.receiverStarkKey, receiverVaultId: signableResponse.receiverVaultId, senderVaultId: signableResponse.senderVaultId, - starkSignature: signatures.starkSignature - ), - ], + starkSignature: signatures.starkSignatures[index] + ) + }, senderStarkKey: response.senderStarkKey ) ) diff --git a/Sources/ImmutableXCore/Workflows/Workflow.swift b/Sources/ImmutableXCore/Workflows/Workflow.swift index 2f7f45a..3baa70f 100644 --- a/Sources/ImmutableXCore/Workflows/Workflow.swift +++ b/Sources/ImmutableXCore/Workflows/Workflow.swift @@ -4,7 +4,34 @@ import Foundation struct WorkflowSignatures { let ethAddress: String let ethSignature: String - let starkSignature: String + let starkSignatures: [String] + + init( + ethAddress: String, + ethSignature: String, + starkSignatures: [String] + ) { + precondition(!starkSignatures.isEmpty, "List cannot be empty") + self.ethAddress = ethAddress + self.ethSignature = ethSignature + self.starkSignatures = starkSignatures + } + + init( + ethAddress: String, + ethSignature: String, + starkSignature: String + ) { + self.init( + ethAddress: ethAddress, + ethSignature: ethSignature, + starkSignatures: [starkSignature] + ) + } + + var starkSignature: String { + starkSignatures.first! + } /// Serialized signature to be supplied to the API var serializedEthSignature: String { diff --git a/Tests/ImmutableXCoreTests/Mocks/Workflows/TransferWorkflowMock.swift b/Tests/ImmutableXCoreTests/Mocks/Workflows/TransferWorkflowMock.swift index ebf2636..26033b9 100644 --- a/Tests/ImmutableXCoreTests/Mocks/Workflows/TransferWorkflowMock.swift +++ b/Tests/ImmutableXCoreTests/Mocks/Workflows/TransferWorkflowMock.swift @@ -18,7 +18,12 @@ class TransferWorkflowMock: TransferWorkflow { companion = nil } - override class func transfer(token: AssetModel, recipientAddress: String, signer: Signer, starkSigner: StarkSigner, transfersAPI: TransfersAPI.Type = TransfersAPI.self) async throws -> CreateTransferResponse { + override class func transfer( + transfers: [TransferData], + signer: Signer, + starkSigner: StarkSigner, + transfersAPI: TransfersAPI.Type = TransfersAPI.self + ) async throws -> CreateTransferResponse { let companion = companion! if let error = companion.throwableError { diff --git a/Tests/ImmutableXCoreTests/Workflows/TransferWorkflowTests.swift b/Tests/ImmutableXCoreTests/Workflows/TransferWorkflowTests.swift index c795077..98de42b 100644 --- a/Tests/ImmutableXCoreTests/Workflows/TransferWorkflowTests.swift +++ b/Tests/ImmutableXCoreTests/Workflows/TransferWorkflowTests.swift @@ -23,8 +23,12 @@ final class TransferWorkflowTests: XCTestCase { func testTransferERC20FlowSuccess() async throws { let response = try await TransferWorkflow.transfer( - token: erc20Token, - recipientAddress: recipientAddress, + transfers: [ + .init( + token: erc20Token, + recipientAddress: recipientAddress + ), + ], signer: SignerMock(), starkSigner: StarkSignerMock(), transfersAPI: transfersAPI @@ -35,8 +39,12 @@ final class TransferWorkflowTests: XCTestCase { func testTransferETHFlowSuccess() async throws { let response = try await TransferWorkflow.transfer( - token: ethToken, - recipientAddress: recipientAddress, + transfers: [ + .init( + token: ethToken, + recipientAddress: recipientAddress + ), + ], signer: SignerMock(), starkSigner: StarkSignerMock(), transfersAPI: transfersAPI @@ -47,8 +55,12 @@ final class TransferWorkflowTests: XCTestCase { func testTransferERC721FlowSuccess() async throws { let response = try await TransferWorkflow.transfer( - token: erc721Token, - recipientAddress: recipientAddress, + transfers: [ + .init( + token: erc721Token, + recipientAddress: recipientAddress + ), + ], signer: SignerMock(), starkSigner: StarkSignerMock(), transfersAPI: transfersAPI @@ -64,8 +76,12 @@ final class TransferWorkflowTests: XCTestCase { await XCTAssertThrowsErrorAsync { _ = try await TransferWorkflow.transfer( - token: erc721Token, - recipientAddress: recipientAddress, + transfers: [ + .init( + token: erc721Token, + recipientAddress: recipientAddress + ), + ], signer: SignerMock(), starkSigner: StarkSignerMock(), transfersAPI: self.transfersAPI @@ -80,8 +96,12 @@ final class TransferWorkflowTests: XCTestCase { await XCTAssertThrowsErrorAsync { _ = try await TransferWorkflow.transfer( - token: erc721Token, - recipientAddress: recipientAddress, + transfers: [ + .init( + token: erc721Token, + recipientAddress: recipientAddress + ), + ], signer: SignerMock(), starkSigner: StarkSignerMock(), transfersAPI: self.transfersAPI