This example shows how the Portal Provider interacts with the blockchain.
Signing an ethereum transaction
The Provider has a address of our MPC wallet. The Provider then proxies the request for balance to the configured RPC url and returns the result.
Portal's web3 provider:
Ensure you have set the gateway URL correctly with Infura or Alchemy when you initialize the portal class.
import PortalSwift
public var portal: Portal?
@IBAction func sendEip155Transaction(_: UIButton) {
Task {
do {
// Ethereum Sepolia
let chainId = "eip155:11155111" // CAIP-2 format.
// Obtain the eip155 address of the user's wallet.
guard let eip155Address = await portal.getAddress(chainId) else {
throw PortalExampleAppError.addressNotFound()
}
// Create the transaction object.
let transaction = [
"from": eip155Address,
"to": "0xRecipientAddress",
"value": "0x10", // Native token amount to send (WEI for eip155:11155111).
]
// Make the eth_sendTransaction request.
let requestResponse = try await portal.request(
chainId,
withMethod: .eth_sendTransaction,
andParams: [transaction]
)
// Obtain the transaction hash.
guard let transactionHash = requestResponse.result as? String else {
throw PortalExampleAppError.invalidResponseTypeForRequest()
}
// ✅ Great! You sent your first transaction successfully.
} catch {
// Handle any errors during the Portal provider request.
}
}
}
In the example above, we use .eth_sendTransaction. However, there's many other PortalRequestMethods available for you to use, which you can find in the SDK.
Estimating Gas
By default, Portal will estimate and populate the gas property in a transaction object if the property is undefined.
To estimate the gas value manually, you'll want to use the eth_estimateGas RPC call and pass in your transaction as the parameter.
import PortalSwift
public var portal: Portal?
@IBAction func estimateGas(_: UIButton) {
Task {
do {
// Ethereum Sepolia
let chainId = "eip155:11155111" // CAIP-2 format.
// Obtain the eip155 address of the user's wallet.
guard let eip155Address = await portal.getAddress(chainId) else {
throw PortalExampleAppError.addressNotFound()
}
// Create the transaction object.
let transaction = [
"from": eip155Address,
"to": "0xRecipientAddress",
"value": "0x10", // Native token amount to send (WEI for eip155:11155111).
]
// Make the eth_estimateGas request with the transaction object.
let requestResponse = try await portal.request(
chainId,
withMethod: .eth_estimateGas,
andParams: [transaction]
)
// Retrieve the estimated gas value.
guard let estimatedGas = requestResponse.result as? String else {
throw PortalExampleAppError.invalidResponseTypeForRequest()
}
// ✅ Nice! You just retrieved the gas estimate.
} catch {
// Handle any errors during the Portal provider request.
}
}
}
Signing Solana Transactions
We offer a portal.sendSol function to make the process of sending sol very simple. If you would like to construct your own more advanced transaction for solana simply add Import SolanaSwift to the top of your file in order to start using this library (it is included in our sdk as a dependency)
Be sure to serialize the transaction with requiredAllSignatures: false. Then Base64 encode this object as a string and pass that in to the portal.request method as the only element in the array of the andParams argument.
...
Import SolanaSwift
func handleSolanaSendTrx() {
Task {
do {
// devnet chainId
let chainId = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"
// Get the solana address
guard let portal = self.portal, let fromAddress = await portal.getAddress(chainId) else {
self.logger.error("ViewController.handleSolanaSendTrx() - ❌ Portal or address not initialized/found")
self.stopLoading()
return
}
// Format the solana address properly
let fromPublicKey = try SolanaSwift.PublicKey(string: fromAddress)
// Set the address you would like to send to
let toPublicKey = try SolanaSwift.PublicKey(string: "2Xc2Z8jKVgSSQaGdCEbspReGbJkWGrEvR6aGmhSZAdjf")
// Create the transfer instruction
let transferInstruction = SystemProgram.transferInstruction(
from: fromPublicKey,
to: toPublicKey,
lamports: 1
)
// Get the most recent blockhash
let blockhashResponse: PortalProviderResult = try await portal.request(chainId, withMethod: .sol_getLatestBlockhash, andParams: [])
guard let blockhashResResult = blockhashResponse.result as? SolGetLatestBlockhashResponse else {
throw PortalSolError.failedToGetLatestBlockhash
}
// Initialize the transaction
var transaction = SolanaSwift.Transaction(
instructions: [transferInstruction],
recentBlockhash: blockhashResResult.result.value.blockhash,
feePayer: fromPublicKey
)
// Format the transaction
let tx = try transaction.serialize(requiredAllSignatures: false)
let txHash: PortalProviderResult = try await portal.request(chainId, withMethod: PortalRequestMethod.sol_signAndSendTransaction, andParams: [tx.base64EncodedString()])
self.logger.info("ViewController.handleSolanaSendTrx() - ✅ Successfully signed message: SOL: \(txHash.result as! String)")
} catch {
self.logger.error("ViewController.handleSolanaSendTrx() - ❌ Generic error: \(error)")
}
}
}
Raw sign
You can now utilize our SDK to generate raw signatures that are generated using the underlying key share without adding any chain specific formatting to the signature. This effectively unlocks your ability to use the Portal SDK with any chain that uses SECP256K1 or ED25519.
do {
let response = try await portal.rawSign(
message: "74657374", // Must be in hex format, e.g. "test" is "74657374" in hex.
chainId: "eip155:11155111" // choose the chainId eip155 = secp256k1, solana = ed25519
)
if let signature = response.result as? String {
print("✅ Successfully signed message: \(signature)")
}
} catch {
print("❌ Error signing message: \(error)")
}
Enabling the Enclave Signer
Executing MPC operations requires computation on the client device. Depending on the CPU of the client device this can take variable amounts of time, leading to inconsistent signing times across users.
To solve this, you can leverage the Enclave MPC API from your SDK to execute MPC operations server-side which leads to consistent (and often faster) signing speeds.
This feature leverages the Enclave MPC API by sending the user's key share to a Trusted Execution Environment (TEE) which runs the MPC code in a secure AWS Nitro Enclave with the same non-custodial guarantees as client-side MPC.
By enabling the useEnclaveMpcApi feature flag the client key share will be transmitted from the user device, but it is never stored.
TEEs in Nitro Enclaves work by encrypting memory and verifying execution. Encrypted memory means that all of the data being processed on the enclave can’t be accessed by anything other than the running application. Portal employees can’t even read the data on there! Verified execution means that a user can cryptographically verify that their request was handled in a secure enclave. When a user sends an API request to the enclave, Portal returns a set of signed “measurements” that can be verified by the enclave’s public key to ensure that the request was processed on an AWS Nitro Enclave.
import PortalSwift
// Initialize Portal with the Enclave MPC API enabled
let portal = try Portal(
"CLIENT_API_KEY_OR_CLIENT_SESSION_TOKEN",
featureFlags: FeatureFlags(
useEnclaveMPCApi: true
)
)
By setting useEnclaveMPCApi to true, the Portal instance will use the Enclave MPC API for signing transactions, ensuring faster computation and consistent performance across client devices.
And now you are signing transactions with Portal! 🙌 🚀 Next, we'll explore how to simulate a transaction so that you can create smoother experiences for your users.