diff --git a/Package.resolved b/Package.resolved index 841ff4e..4b9fe39 100644 --- a/Package.resolved +++ b/Package.resolved @@ -22,10 +22,19 @@ { "package": "SolanaSwift", "repositoryURL": "https://github.com/p2p-org/solana-swift.git", + "state": { + "branch": "feature/universal-add-signature", + "revision": "9ffd12fcadba74cfd795f10a6f47a4df74864367", + "version": null + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", "state": { "branch": null, - "revision": "596dab76f1e82f8e6c5d6b64534d989b2ba47f69", - "version": "2.5.2" + "revision": "9f39744e025c7d377987f30b03770805dcb0bcd1", + "version": "1.1.4" } }, { diff --git a/Package.swift b/Package.swift index 918c478..416c430 100644 --- a/Package.swift +++ b/Package.swift @@ -14,13 +14,26 @@ let package = Package( products: [ .library( name: "FeeRelayerSwift", - targets: ["FeeRelayerSwift"]), + targets: ["FeeRelayerSwift"] + ), + .executable( + name: "FeeRelayerCLI", + targets: ["FeeRelayerCLI"] + ), ], dependencies: [ - .package(url: "https://github.com/p2p-org/solana-swift.git", from: "2.5.2"), - .package(url: "https://github.com/p2p-org/OrcaSwapSwift.git", from: "2.1.1") + .package(url: "https://github.com/p2p-org/solana-swift.git", branch: "feature/universal-add-signature"), + .package(url: "https://github.com/p2p-org/OrcaSwapSwift.git", from: "2.1.1"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.4"), ], targets: [ + .executableTarget( + name: "FeeRelayerCLI", + dependencies: [ + "FeeRelayerSwift", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ] + ), .target( name: "FeeRelayerSwift", dependencies: [ diff --git a/Sources/FeeRelayerCLI/cli.swift b/Sources/FeeRelayerCLI/cli.swift new file mode 100644 index 0000000..d790d8a --- /dev/null +++ b/Sources/FeeRelayerCLI/cli.swift @@ -0,0 +1,209 @@ +// Copyright 2022 P2P Validator Authors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. + +import ArgumentParser +import FeeRelayerSwift +import Foundation +import OrcaSwapSwift +import SolanaSwift + +@main +enum App { + static func main() async { + await FeeRelayerCommand.main() + } +} + +struct FeeRelayerCommand: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "A Swift command-line tool for API Gateway", + subcommands: [CreateAccount.self, RelaySend.self] + ) +} + +struct CreateAccount: AsyncParsableCommand { + @Option(help: "Wallet") + var wallet: String = "5bYReP8iw5UuLVS5wmnXfEfrYCKdiQ1FFAZQao8JqY7V" + + @Option(help: "To address") + var mint: String = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + + @Option(help: "Transfer amount") + var amount: UInt64 = 5000 + + @Option(help: "Bloch hash") + var blochHash: String? + + @Option(help: "Fee payer") + var feePayer: String = "FG4Y3yX4AAchp1HvNZ7LfzFTewF2f6nDoMDCohTFrdpT" + + @Option(help: "Lamport per signature") + var lamportPerSignature: UInt64 = 5000 + + @Option(help: "Lamport per signature") + var rentExemption: UInt64 = 0 + + @Option(help: "Fee relayer endpoint") + var feeRelayerEndpoint: String = "https://fee-relayer.key.app" + + @Option(help: "Fee relayer endpoint") + var solanaEndpoint: String = "https://api.mainnet-beta.solana.com" + + @Flag(help: "cURL") + var curl: Bool = false + + func run() async throws { + let solana = JSONRPCAPIClient(endpoint: .init(address: solanaEndpoint, network: .mainnetBeta)) + var recentBlochHash = blochHash + if recentBlochHash == nil { + recentBlochHash = try await solana.getRecentBlockhash() + } + + let from = try PublicKey(string: wallet) + let mint = try PublicKey(string: mint) + let to = try PublicKey.associatedTokenAddress(walletAddress: from, tokenMintAddress: mint) + + let transaction = Transaction( + instructions: [ + SystemProgram.createAccountInstruction( + from: try PublicKey(string: feePayer), + toNewPubkey: to, + lamports: amount, + space: 165, + programId: TokenProgram.id + ), + SystemProgram.transferInstruction( + from: try PublicKey(string: wallet), + to: try PublicKey(string: feePayer), + lamports: 5000 + ), + ], + recentBlockhash: recentBlochHash, + feePayer: try PublicKey(string: feePayer) + ) + + let preparedTransaction = PreparedTransaction( + transaction: transaction, + signers: [], + expectedFee: .init(transaction: lamportPerSignature, accountBalances: rentExemption) + ) + + var httpClient: HTTPClient = FeeRelayerHTTPClient() + if curl { + httpClient = CURLHTTPClient() + } + + let apiClient = FeeRelayerSwift.APIClient(httpClient: httpClient, baseUrlString: feeRelayerEndpoint, version: 1) + do { + let result = try await apiClient.sendTransaction(.signRelayTransaction(.init(preparedTransaction: preparedTransaction))) + print(result) + } catch is CURLHTTPClient.Error { + return + } catch { + print(error) + } + } +} + +struct RelaySend: AsyncParsableCommand { + @Option(help: "Source address") + var from: String = "5bYReP8iw5UuLVS5wmnXfEfrYCKdiQ1FFAZQao8JqY7V" + + @Option(help: "Source address") + var seedPhrase: String? + + @Option(help: "Destination address") + var to: String = "9zRnk58ydEKxQ4BKyETG8uQQecppcxMvQaJWLkjocvPm" + + @Option(help: "Transfer amount") + var amount: UInt64 = 5000 + + @Option(help: "Bloch hash") + var blochHash: String? + + @Option(help: "Fee payer") + var feePayer: String = "FG4Y3yX4AAchp1HvNZ7LfzFTewF2f6nDoMDCohTFrdpT" + + @Option(help: "Lamport per signature") + var lamportPerSignature: UInt64 = 10000 + + @Option(help: "Lamport per signature") + var rentExemption: UInt64 = 0 + + @Option(help: "Fee relayer endpoint") + var feeRelayerEndpoint: String = "https://fee-relayer.key.app" + + @Option(help: "Fee relayer endpoint") + var solanaEndpoint: String = "https://api.mainnet-beta.solana.com" + + @Flag(help: "cURL") + var curl: Bool = false + + func run() async throws { + let solana = JSONRPCAPIClient(endpoint: .init(address: solanaEndpoint, network: .mainnetBeta)) + var recentBlochHash = blochHash + if recentBlochHash == nil { + recentBlochHash = try await solana.getRecentBlockhash() + } + + var transaction = Transaction( + instructions: [ + SystemProgram.transferInstruction( + from: try PublicKey(string: from), + to: try PublicKey(string: to), + lamports: amount + ), + ], + recentBlockhash: recentBlochHash, + feePayer: try PublicKey(string: feePayer) + ) + + + + var signers: [Account] = [] + if let seedPhrase = seedPhrase { + let account = try await Account(phrase: seedPhrase.components(separatedBy: " "), network: .mainnetBeta, derivablePath: .default) + try transaction.sign(signers: [account]) + signers.append(account) + } + + let preparedTransaction = PreparedTransaction( + transaction: transaction, + signers: signers, + expectedFee: .init(transaction: lamportPerSignature, accountBalances: rentExemption) + ) + + var httpClient: HTTPClient = FeeRelayerHTTPClient() + if curl { + httpClient = CURLHTTPClient() + } + + let apiClient = FeeRelayerSwift.APIClient(httpClient: httpClient, baseUrlString: feeRelayerEndpoint, version: 1) + do { + let result = try await apiClient.sendTransaction(.signRelayTransaction(.init(preparedTransaction: preparedTransaction))) + try transaction.addSignature(.init(signature: Data(Base58.decode(result)), publicKey: try .init(string: feePayer))) + + // print(transaction.jsonString) + // print(Base58.encode(try transaction.serialize(requiredAllSignatures: true, verifySignatures: true))) + print(try transaction.serialize().base64EncodedString()) + } catch is CURLHTTPClient.Error { + return + } catch { + print(error) + } + } +} + +class CURLHTTPClient: HTTPClient { + enum Error: Swift.Error { + case stop + } + + var networkManager: FeeRelayerSwift.NetworkManager = URLSession.shared + + func sendRequest(request: URLRequest, decoder _: JSONDecoder) async throws -> T where T: Decodable { + print(request.cURL()) + throw Error.stop + } +}