Safari Extension Walletlib
About
A two part SDK that facilitates a simple Wallet RPC scheme for Javascript to Swift communication within a Safari Web Extension for iOS. The SDK is comprised two separate libraries: a Javascript library and a Swift library. The library is designed to be extendable and allow wallets to add their own customized RPC methods.
Installation
The consumer of this SDK will need to install both the Javascript NPM package and the Swift podfile. Although optional, these two libraries conveniently share the same RPC spec.
In the Javascript project of your Safari Extension, run:
npm install <TODO: Publish npm package>
In the Swift Safari Extension target of your XCode project, run:
pod install <TODO: Publish Podfile>
RPC methods
The SDK provides an implementation of two initial Wallet RPC requests for convenience:
- GetAccounts
- SignPayloads
Using them is optional and the wallet can create their own custom RPC requests, explained in the next section.
These RPC methods are defined by a JSON Serializable Parameter and Result type. These types will are defined equivalently in both the Javascript library and the Swift library.
GetAccounts
This request instructs the Swift handler to return the accounts (or public key) associated with the requesting user.
-
Convenience JS function:
sendNativeGetAccountsRequest
, for sending GetAccounts request and parsing the response.
SignPayloads
This request instructs the Swift handler to sign the payloads
with the private key that corresponds to the provided address
.
-
Convenience JS function:
sendNativeSignPayloadsRequest
, for sending SignPayloads request and parsing the response.
Usage: Javascript
The Javascript library exposes methods for sending RPC requests to the Swift Extension handler.
Example: Request Sign Paylaods
Use the convenience function sendNativeSignPayloadsRequest
to send a SignPayloads request and receive
a parsed result.
import {
sendNativeSignPayloadsRequest,
NativeSignPayloadsResult,
} from "safari-extension-walletlib";
try {
const result: NativeSignPayloadsResult = await sendNativeSignPayloadsRequest({
address: someBase64EncodedAddress,
payloads: [someBase64EncodedPayload],
});
console.log(result.signed_payloads);
} catch (err) {
console.error(err.message);
}
Usage: Swift
The Swift library provides extension methods on the NSExtensionContext
to parse these RPC requests and respond back to the Javascript side.
requestMethod() -> String?
- Retrieves the RPC request's method name. Returns
nil
if unable to parse.
decodeRpcRequestParameter<T: Decodable>(toType type: T.Type) -> T?
- Decodes JSON string parameters from the RPC request into a specified
Decodable
typeT
. Returns an instance ofT
ornil
on failure.
completeRpcRequestWith(result: Encodable)
- Completes an RPC request with a success result. Encodes the result to JSON and responds to Javascript. On failure to encode, it completes the request with an error.
completeRpcRequestWith(errorMessage: String)
- Completes an RPC request with an error message.
Example: Handling requests
The wallet can use these methods in the beginRequest
in its SafariWebExtensionHandler
to handle incoming requests.
import SafariExtensionWalletlibSwift
func beginRequest(with context: NSExtensionContext) {
// Parse the method identifier from `context`
guard let method = context.requestMethod() else {
context.completeRpcRequestWith(errorMessage: "Error parsing method")
return
}
switch method {
case GET_ACCOUNTS_REQUEST_METHOD:
context.completeRpcRequestWith(result: GetAccountsResult(addresses: ["encoded_address"]))
return
case SIGN_PAYLOADS_REQUEST_METHOD:
// Decode SignPayloads params
guard let params: SignPayloadsParams = context.decodeRpcRequestParameter(toType: SignPayloadsParams.self),
!params.payloads.isEmpty else {
context.completeRpcRequestWith(errorMessage: "Error parsing Sign Payloads request")
return
}
// Implement signing then complete the request with result
context.completeRpcRequestWith(result: SignPayloadsResult(signed_payloads: ["<encoded_signed_payload>"]))
return
default:
context.completeRpcRequestWith(errorMessage: "Unsupported method")
return
}
}
Adding custom RPC requests
In addition to GetAccounts and SignPayloads, a wallet might need more customized RPC requests.
If so, the wallet can add new RPC requests by defining JSON serializable Parameter and Result data models that are equivalent in Javascript and Swift.
Define the request model in Javascript
Define a method identifier, parameters, and a result type. Ensure that these types are JSON serializable. The JSONObject
helper type conforms to this.
// 1. Define a method identifier
export const MY_CUSTOM_PING_RPC_METHOD = "MY_CUSTOM_PING_RPC_METHOD";
// 2. Define JSON serializable parameters of your request
type MyCustomPingRequestParams = {
pingParam1: string;
pingParam2: string[];
};
// 3. Define the expected JSON serializable result of your request.
type MyCustomPingRequestResult = {
pong: string;
};
Define the request model in Swift
// 1. Define an equivalent method identifier
public let MY_CUSTOM_PING_RPC_METHOD = "MY_CUSTOM_PING_RPC_METHOD"
// 2. Define an equivalent parameters struct that conforms to `Decodable`
public struct MyCustomPingRequestParams: Decodable {
let pingParam1: String
let pingParam2: [String]
}
// 3. Define an equivalent result struct that conforms to `Encodable`
public struct MyCustomPingRequestResult: Encodable {
let pong: String
public init(pong: String) {
self.pong = pong
}
}
Send the request
Then, use the generic RPC sender function sendNativeRpcRequest
to send the request to the Swift side.
const nativeResponse: NativeRpcResponse = await sendNativeRpcRequest({
method: MY_CUSTOM_RPC_METHOD,
params: {
customParam1: "ping",
customParam2: ["ping"],
},
});
// Unwrap the RpcResponse and attempt to parse
if (nativeResponse.result) {
const resultObj: MyCustomPingRequestResult = JSON.parse(
nativeResponse.result
);
console.log(resultObj.pong); // "pong!"
} else {
console.error(nativeResponse.error);
}
Respond to the request
In the Swift Extension Handler, the wallet can filter and parse this custom RPC request, then handle accordingly.
func beginRequest(with context: NSExtensionContext) {
guard let method = context.requestMethod() else {
context.completeRpcRequestWith(errorMessage: "Unsupported method")
return
}
switch method {
case MY_CUSTOM_RPC_METHOD:
context.completeRpcRequestWith(result: MyCustomPingRequestResult(pong: "pong!"))
default:
context.completeRpcRequestWith(errorMessage: "Unsupported method")
}
}