Skip to content

Commit

Permalink
Add test + example for signing/sending transcations
Browse files Browse the repository at this point in the history
  • Loading branch information
taylorjdawson committed May 14, 2024
1 parent 15130e5 commit 5817374
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 6 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
API_PUBLIC_KEY="<turnkey-api-public-key>"
API_PRIVATE_KEY="<turnkey-api-private-key>"
ORGANIZATION_ID="<turnkey-organization-id>"
# The user ID of the user associated with the given organization id
EXPECTED_USER_ID=<expected-user-id>

# You can find the private key ID and the associated wallet address in the Turnkey dashboard
WALLET_FROM_ADDRESS=<wallet-from-address>
PRIVATE_KEY_ID=<private-key-id>

INFURA_API_KEY=<infura-api-key>
108 changes: 108 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
"version" : "5.3.0"
}
},
{
"identity" : "cryptoswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : {
"revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0",
"version" : "1.8.2"
}
},
{
"identity" : "openapikit",
"kind" : "remoteSourceControl",
Expand All @@ -36,6 +45,24 @@
"version" : "3.1.3"
}
},
{
"identity" : "promisekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mxcl/PromiseKit.git",
"state" : {
"revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d",
"version" : "6.22.1"
}
},
{
"identity" : "secp256k1.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Boilertalk/secp256k1.swift.git",
"state" : {
"revision" : "cd187c632fb812fd93711a9f7e644adb7e5f97f0",
"version" : "0.1.7"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
Expand All @@ -54,6 +81,15 @@
"version" : "1.3.1"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
}
},
{
"identity" : "swift-bigint",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -108,6 +144,51 @@
"version" : "1.0.3"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "359c461e5561d22c6334828806cc25d759ca7aa6",
"version" : "2.65.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63",
"version" : "1.22.0"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "c6afe04165c865faaa687b42c32ed76dfcc91076",
"version" : "1.31.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5",
"version" : "2.26.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce",
"version" : "1.20.1"
}
},
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -144,6 +225,15 @@
"version" : "1.0.1"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "025bcb1165deab2e20d4eaba79967ce73013f496",
"version" : "1.2.1"
}
},
{
"identity" : "valet",
"kind" : "remoteSourceControl",
Expand All @@ -153,6 +243,24 @@
"version" : "4.3.0"
}
},
{
"identity" : "web3.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Boilertalk/Web3.swift.git",
"state" : {
"revision" : "b85187ddf230a10b04fa8574c44980f3369ddff5",
"version" : "0.8.8"
}
},
{
"identity" : "websocket-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/websocket-kit",
"state" : {
"revision" : "4232d34efa49f633ba61afde365d3896fc7f8740",
"version" : "2.15.0"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
Expand Down
10 changes: 8 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ let package = Package(
.package(url: "https://github.com/mkrd/Swift-BigInt.git", from: "2.0.0"),
.package(url: "https://github.com/anquii/Base58Check.git", from: "1.0.0"),
.package(url: "https://github.com/Square/Valet", from: "4.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/Boilertalk/Web3.swift.git", from: "0.6.0")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand Down Expand Up @@ -57,7 +58,12 @@ let package = Package(
),
.testTarget(
name: "TurnkeySDKTests",
dependencies: ["TurnkeySDK", .product(name: "SwiftDotenv", package: "swift-dotenv")]),
dependencies: ["TurnkeySDK",
.product(name: "SwiftDotenv", package: "swift-dotenv"),
.product(name: "Web3", package: "Web3.swift"),
.product(name: "Web3PromiseKit", package: "Web3.swift"),
]
),
// Empty target that builds the DocC catalog at /SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc.
// The SwiftDocCPlugin catalog includes high-level, user-facing documentation about using
// the Swift-DocC plugin from the command-line.
Expand Down
166 changes: 164 additions & 2 deletions Tests/TurnkeySDKTests/TurnkeySDKTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import CryptoKit
import OpenAPIRuntime
import OpenAPIURLSession
import SwiftDotenv
import Web3
import Web3PromiseKit
import XCTest

@testable import Shared
Expand All @@ -9,6 +13,10 @@ final class TurnkeySDKTests: XCTestCase {
var apiPrivateKey: String?
var apiPublicKey: String?
var organizationId: String?
var privateKeyId: String?
var walletFromAddress: String?
var expectedUserId: String?
var infuraAPIKey: String?

override func setUp() {
super.setUp()
Expand All @@ -19,10 +27,17 @@ final class TurnkeySDKTests: XCTestCase {
apiPrivateKey = Dotenv.apiPrivateKey?.stringValue ?? ""
apiPublicKey = Dotenv.apiPublicKey?.stringValue ?? ""
organizationId = Dotenv.organizationId?.stringValue ?? ""
privateKeyId = Dotenv.privateKeyId?.stringValue ?? ""
infuraAPIKey = Dotenv.infuraAPIKey?.stringValue ?? ""
walletFromAddress = Dotenv.walletFromAddress?.stringValue ?? ""
expectedUserId = Dotenv.expectedUserId?.stringValue ?? ""
// Check if required environment variables are defined
guard apiPrivateKey != "",
apiPublicKey != "",
organizationId != ""
organizationId != "",
privateKeyId != "",
infuraAPIKey != "",
walletFromAddress != ""
else {
XCTFail("Required environment variables are not defined.")
return
Expand All @@ -47,7 +62,7 @@ final class TurnkeySDKTests: XCTestCase {
// Assert the expected properties in the whoamiResponse
XCTAssertNotNil(whoamiResponse.organizationId)
XCTAssertEqual(whoamiResponse.organizationName, "SDK E2E")
XCTAssertEqual(whoamiResponse.userId, "c1fe55f0-28b7-450b-8cb6-47d175cb66f5")
XCTAssertEqual(whoamiResponse.userId, expectedUserId!)
XCTAssertEqual(whoamiResponse.username, "Root user")
// Add more assertions based on the expected response
}
Expand Down Expand Up @@ -174,4 +189,151 @@ final class TurnkeySDKTests: XCTestCase {
XCTFail("Undocumented response: \(statusCode)")
}
}

func bodyToString(body: HTTPBody) -> Promise<String> {
return Promise { seal in
Task {
do {
let bodyString = try await String(collecting: body, upTo: .max)
seal.fulfill(bodyString)
} catch {
seal.reject(error)
}
}
}
}

func testSignTransactionWithWeb3() async throws {
let expectation = XCTestExpectation(description: "Sign transaction and handle response")

// Setup the Ethereum private key and web3 instance
let web3 = Web3(rpcURL: "https://holesky.infura.io/v3/\(infuraAPIKey ?? "")") // Replace with actual URL and project ID
let from = try! EthereumAddress(hex: walletFromAddress ?? "", eip55: true)

firstly {
web3.eth.getTransactionCount(address: from, block: .latest)
}.then { nonce -> Promise<String> in
let transaction = EthereumTransaction(
nonce: nonce,
maxFeePerGas: EthereumQuantity(quantity: 21.gwei),
maxPriorityFeePerGas: EthereumQuantity(quantity: 1.gwei),
gasLimit: 29000,
to: try! EthereumAddress(hex: "0x518AC04a5Bbc5846F0de774458565Ad5957c9017", eip55: true),
value: EthereumQuantity(quantity: 1000.gwei),
transactionType: .eip1559
)

let zeroQuantity = EthereumQuantity(integerLiteral: 0).quantity
let maxPriorityFee = transaction.maxPriorityFeePerGas?.quantity ?? zeroQuantity
let maxFeePerGas = transaction.maxFeePerGas?.quantity ?? zeroQuantity
let gasLimit = transaction.gasLimit?.quantity ?? zeroQuantity
let toAddress = transaction.to?.rawAddress ?? Bytes()
let transactionValue = transaction.value?.quantity ?? zeroQuantity
let transactionType = transaction.transactionType

// Create an RLPItem representing the transaction for encoding
// Important: Order matters here:
let rlpItem: RLPItem = RLPItem.array([
.bigUInt(EthereumQuantity(integerLiteral: 17000).quantity),
.bigUInt(nonce.quantity),
.bigUInt(maxPriorityFee),
.bigUInt(maxFeePerGas),
.bigUInt(gasLimit),
.bytes(toAddress),
.bigUInt(transactionValue),
.bytes(Bytes()), // input data
.array([]), // Access list
])

// Serialize the RLPItem
let serializedTransaction = try RLPEncoder().encode(rlpItem)
// Append "02" for EIP-1159 transactions
let transactionHexString =
"02" + serializedTransaction.map { String(format: "%02x", $0) }.joined()

return Promise.value(transactionHexString)

}.then { transactionHexString -> Promise<Operations.SignTransaction.Output> in
Promise { seal in
Task {
do {
let client = TurnkeyClient(
apiPrivateKey: self.apiPrivateKey!, apiPublicKey: self.apiPublicKey!)
let response = try await client.signTransaction(
organizationId: self.organizationId!,
signWith: self.privateKeyId!,
unsignedTransaction: transactionHexString,
_type: .TRANSACTION_TYPE_ETHEREUM
)
seal.fulfill(response)
} catch {
seal.reject(error)
}
}
}
}.then { response -> Promise<String> in
switch response {
case let .ok(response):
switch response.body {
case let .json(activityResponse):
if let signedTransaction = activityResponse.activity.result.signTransactionResult?
.signedTransaction
{
return Promise.value(signedTransaction)
} else {
return Promise(
error: NSError(
domain: "SignTransactionError", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Signed transaction is nil"]))
}
}
case let .undocumented(statusCode, undocumentedPayload):
if let body = undocumentedPayload.body {
return self.bodyToString(body: body).then { bodyString in
XCTFail("Undocumented response body: \(bodyString)")
return Promise<String>(
error: NSError(
domain: "UndocumentedResponseError", code: statusCode,
userInfo: [NSLocalizedDescriptionKey: "Undocumented response body: \(bodyString)"]))
}
}
XCTFail("Undocumented response with status code: \(statusCode)")
return Promise(
error: NSError(
domain: "UndocumentedResponseError", code: statusCode,
userInfo: [
NSLocalizedDescriptionKey: "Undocumented response with status code: \(statusCode)"
]))
}
}.then { signedTransaction -> Promise<String> in
let request = BasicRPCRequest(
id: 0,
jsonrpc: Web3.jsonrpc,
method: "eth_sendRawTransaction",
params: ["0x" + signedTransaction]
)
return Promise { seal in
web3.provider.send(request: request) { (response: Web3Response<EthereumData>) in
switch response.status {
case let .success(result):
// print("Transaction hash: \(result.hex())")
seal.fulfill(result.hex())
case let .failure(error):
seal.reject(error)
}
}
}
}
.done { hash in
print("Transaction hash - \(hash)")
XCTAssertNotNil(hash, "Transaction hash should not be nil")
expectation.fulfill()
}.catch { error in
XCTFail("Failed: \(error)")
expectation.fulfill()
}

// Wait for the expectation to be fulfilled, or timeout after 10 seconds
await fulfillment(of: [expectation], timeout: 10.0)
}
}
2 changes: 1 addition & 1 deletion docs/proxy-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ This setup is especially useful for operations like:

## Conclusion

While `ProxyMiddleware` is not required, it provides a flexible and powerful way to manage preliminary request handling and authentication, providing a flexible and secure way to integrate with Turnkey's services.
While `ProxyMiddleware` is not required, it provides a convenient way to send requests on behalf of unauthenticated users looking to perform
Loading

0 comments on commit 5817374

Please sign in to comment.