Safari-Wallet / ui

https://safari-wallet-test-dapp.vercel.app
GNU General Public License v3.0
40 stars 11 forks source link

Swift <> TypeScript Messaging #44

Closed jamierumbelow closed 2 years ago

jamierumbelow commented 2 years ago

This PR introduces a fixed format for messaging between Swift and TypeScript.

Messaging between Swift and Typescript is governed by a fixed and typed interface. The basic object sent over the wire is of the form:

{
  "method": string,
  "params": object
}

Typings need to be defined on both sides of the interface.

TypeScript

On the TypeScript side, messages are defined in src/messaging/index.ts and the src/messaging/messages/*.ts files.

src/messaging/index.ts stores some utility types, and the NativeMessages type, which maps method names (its keys) to a message object.

The src/messaging/messages/*.ts files store the individual message objects, which specify the shape of the message, most importantly its parameters:

export type EthGetBalanceMessage = {
    method: "eth_getBalance";
    params: {
        address: string;
        block?: string;
    };
}

It is then added to the NativeMessages object like so:

export type NativeMessages = {
    eth_getBalance: EthGetBalanceMessage;

This exposes the message to the Messenger interface, which gives you eg type checking:

const balance = await Messenger.sendToNative(
    'eth_getBalance',
    sessionId,
    {
        unknownProperty: "foo" // Error: Property 'unknownProperty' is missing in type 'EthGetBalanceMessage'
    }
);

The Messenger bus will handle serialisation, validation, error checking etc.

Swift

On the Swift side, messages are defined in Messages.swift and the Messages/*.swift files.

Messages.swift defines the NativeMessage and NativeMessageMethod types, and the NativeMessageParams protocol.

The Messages/*.swift files define the individual message params structs, which provide typing around each message's params:

struct helloFrenMessageParams: NativeMessageParams {
    let foo: String
    let bar: Int
    let wagmi: Bool?

    func execute(with userSettings: UserSettings) async throws -> Any {
        if let wagmi = self.wagmi {
            return wagmi ? "wagmi" : "ngmi"
        }
        return "ngmi"
    }
}

The message param structs also implement the execute method, which is called by the SafariWebExtensionHandler#handle method. execute is passed the user settings, and returns a promise that resolves to the result of the message.

This allows the message params struct to pass on control to other parts of the Swift extension or core library in a type-safe way:

struct SomeParamsObject: NativeMessageParams {
    func execute(with userSettings: UserSettings) async throws -> Any {
        doSomething(with: self)
    }
}

func doSomething(with params: SomeParamsObject) {
    // ...
}

Whatever is resolved from the execute method is returned as a JSON response, wrapped in a message key.

NativeMessageParams adheres to the Decodable protocol, which allows you to customise the decoding behaviour:

struct MessageWithDefaultParameterMessageParams: NativeMessageParams {
    let foo: String

    private enum CodingKeys: CodingKey {
        case foo
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let foo = try container.decode(String.self, forKey: .address) {
            self.foo = foo
        } else {
            self.foo = "default"
        }
    }
}

Explicitly not included in this PR, but up next, is:

cloudflare-workers-and-pages[bot] commented 2 years ago

Deploying with  Cloudflare Pages  Cloudflare Pages

Latest commit: 65ba192
Status: ✅  Deploy successful!
Preview URL: https://dcc9f962.ui-eff.pages.dev

View logs

jamierumbelow commented 2 years ago

Excellent idea – I've added it to the todo list :)