@oasisprotocol/sapphire-viem-v2
Version:
Viem support for the Oasis Sapphire ParaTime.
150 lines • 5.52 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { KeyFetcher, NETWORKS as SapphireNETWORKS, wrapEthereumProvider, } from "@oasisprotocol/sapphire-paratime";
import { http, defineChain, serializeTransaction } from "viem";
/**
* sapphire-localnet chain, a local chain for local people
*/
export const sapphireLocalnet = defineChain({
id: SapphireNETWORKS.localnet.chainId,
name: "Oasis Sapphire Localnet",
network: "sapphire-localnet",
nativeCurrency: { name: "Sapphire Local Rose", symbol: "TEST", decimals: 18 },
rpcUrls: {
default: {
http: [SapphireNETWORKS.localnet.defaultGateway],
//webSocket: ["ws://localhost:8546/ws"],
},
},
testnet: true,
});
// Hidden property used to detect if transport is Sapphire-wrapped
export const SAPPHIRE_WRAPPED_VIEM_TRANSPORT = Symbol("#SAPPHIRE_WRAPPED_VIEM_TRANSPORT");
/**
* Provide a Sapphire encrypted RPC transport for Wagmi or Viem.
*
* Example:
* ```
* import { createConfig } from 'viem';
* import { sapphireHttpTransport } from '@oasisprotocol/sapphire-viem-v2';
*
* export const config = createConfig({
* transports: {
* [sapphireTestnet.id]: sapphireHttpTransport()
* },
* ...
* });
* ```
*
* Results for every instance of sapphireHttpTransport() are cached to prevent
* the wrapper from being instantiated multiple times.
*
* @returns Same as http()
*/
export function sapphireHttpTransport(sapphireConfig, overrideUrl, httpConfig) {
const cachedProviders = {};
return ((params) => {
const defaultUrl = params.chain?.rpcUrls.default.http[0];
const url = overrideUrl || defaultUrl;
if (!url) {
throw new Error("sapphireHttpTransport() needs a chain.rpcUrls.default.http[0] to be set or explicit url");
}
if (!(url in cachedProviders)) {
const x = wrapEthereumProvider(http(url, httpConfig)(params), sapphireConfig);
Reflect.set(x, SAPPHIRE_WRAPPED_VIEM_TRANSPORT, true);
cachedProviders[url] = x;
}
return cachedProviders[url];
});
}
/**
* Creates a Viem transaction serializer which encrypts transactions prior to
* them being signed. This is compatible with both local wallet clients and
* injected wallets.
*
* Example
* ```
* import { defineChain } from 'viem';
* import { createSapphireSerializer } from '@oasisprotocol/sapphire-viem-v2';
*
* defineChain({
* serializers: {
* transaction: createSapphireSerializer(publicClient)
* },
* ...
* });
* ```
*
* @param client Provides upstream access to Sapphire JSON-RPC via `.request`
* @param originalSerializer Optional serializer to wrap, otherwise will use default
* @returns Sapphire wrapped transaction encryption serializer
*/
export async function createSapphireSerializer(client, originalSerializer) {
// Don't double-wrap serializer
if (originalSerializer &&
Reflect.has(originalSerializer, SAPPHIRE_WRAPPED_VIEM_SERIALIZER)) {
return originalSerializer;
}
// As the serializer is synchronous, fetching keys while running
const fetcher = new KeyFetcher();
const provider = client;
await fetcher.fetch(provider);
// The fetcher runs in the background, routinely fetching the keys
// This means when the serializer requests a calldata public key one will
// have been retrieved pre-emptively.
const intervalId = setInterval(async () => {
await fetcher.fetch(provider);
}, fetcher.timeoutMilliseconds);
// The interval ID is unreferenced to prevent Node from hanging at exit
// See discussion on https://github.com/oasisprotocol/sapphire-paratime/pull/379
// This is only available in NodeJS, and not in browsers
if (typeof intervalId.unref === "function") {
intervalId.unref();
}
const wrappedSerializer = ((tx, sig) => {
if (!sig) {
const cipher = fetcher.cipherSync();
const encryptedData = cipher.encryptCall(tx.data);
tx.data = encryptedData;
}
if (originalSerializer) {
return originalSerializer(tx, sig);
}
return serializeTransaction(tx, sig);
});
Reflect.set(wrappedSerializer, SAPPHIRE_WRAPPED_VIEM_SERIALIZER, true);
return wrappedSerializer;
}
// Hidden property to test if serializer is Sapphire-wrapped
export const SAPPHIRE_WRAPPED_VIEM_SERIALIZER = Symbol("#SAPPHIRE_WRAPPED_VIEM_SERIALIZER");
/**
* Add the Sapphire transaction encryption wrapper to a wallet client
*
* Example:
* ```
* walletClient = await wrapWalletClient(createWalletClient({
* account,
* chain: sapphireLocalnet,
* transport: sapphireHttpTransport()
* }));
* ```
*
* @param client Wagmi wallet client
* @returns wrapped wallet client
*/
export async function wrapWalletClient(client) {
if (!client.chain) {
throw new Error("No chain defined in client");
}
const originalSerializer = client.chain?.serializers?.transaction;
// Override any existing transaction serializer, or create a new one
// With one that auto-encrypts transactions before they're signed
if (!originalSerializer ||
!Reflect.get(originalSerializer, SAPPHIRE_WRAPPED_VIEM_SERIALIZER)) {
if (!client.chain.serializers) {
client.chain.serializers = {};
}
client.chain.serializers.transaction = await createSapphireSerializer(client, originalSerializer);
}
return client;
}
//# sourceMappingURL=index.js.map