viem
Version:
474 lines (421 loc) • 15.2 kB
text/typescript
import * as Address from 'ox/Address'
import * as Hash from 'ox/Hash'
import * as Hex from 'ox/Hex'
import * as Provider from 'ox/Provider'
import * as RpcRequest from 'ox/RpcRequest'
import type { LocalAccount } from '../accounts/types.js'
import { getTransactionReceipt } from '../actions/public/getTransactionReceipt.js'
import { sendTransaction } from '../actions/wallet/sendTransaction.js'
import { sendTransactionSync } from '../actions/wallet/sendTransactionSync.js'
import { createClient } from '../clients/createClient.js'
import {
createTransport,
type Transport,
} from '../clients/transports/createTransport.js'
import {
type HttpTransport,
type HttpTransportConfig,
http as http_,
} from '../clients/transports/http.js'
import {
MethodNotFoundRpcError,
MethodNotSupportedRpcError,
} from '../errors/rpc.js'
import type { Chain } from '../types/chain.js'
import type { ChainConfig } from './chainConfig.js'
import * as Multisig from './Multisig.js'
import type { Store } from './Store.js'
import * as Store_ from './Store.js'
import * as Transaction from './Transaction.js'
export type HttpConfig = Omit<
HttpTransportConfig,
'batch' | 'raw' | 'rpcSchema'
> & {
/** Store for reading Zone authorization tokens. Defaults to sessionStorage (web) or memory (server). */
store?: Store | undefined
}
/**
* Creates an HTTP transport with support for Zone authentication tokens.
*
* Reads the authorization token from the store and injects the
* `X-Authorization-Token` header on every request.
*
* @example
* ```ts
* import { createPublicClient } from 'viem'
* import { http, Zone } from 'viem/tempo'
*
* const client = createPublicClient({
* chain: Zone.a,
* transport: http(),
* })
* ```
*/
export function http(
url?: string | undefined,
config: HttpConfig = {},
): HttpTransport {
const { store: store_, onFetchRequest, ...rest } = config
const store = store_ ?? Store_.defaultStore()
return (config) =>
http_(url, {
...rest,
async onFetchRequest(request, init) {
const next = (await onFetchRequest?.(request, init)) ?? init
const headers = new Headers(next.headers)
const chainId = config.chain?.id
if (chainId) {
const token = (await store.getItem(`auth:token:${chainId}`)) ?? null
if (token) headers.set('X-Authorization-Token', token)
}
return { ...next, headers }
},
})(config)
}
type RelayProxyParameters = {
/** Policy for how the relay should handle sponsored transactions. Defaults to `'sign-only'`. */
policy?: 'sign-only' | 'sign-and-broadcast' | undefined
}
export type FeePayer = Transport<typeof withFeePayer.type>
export type Relay = Transport<typeof withRelay.type, { multisig: true }>
/**
* Wraps a transport with native multisig request handling.
*
* @example
* ```ts
* import { http, Store, withMultisig } from 'viem/tempo'
*
* const transport = withMultisig(http(), {
* store: Store.memory(),
* })
* ```
*
* @param transport - Transport to wrap.
* @param parameters - Multisig request handler parameters.
* @returns The wrapped transport.
* @experimental
*/
export function withMultisig<transport extends Transport>(
transport: transport,
parameters: withMultisig.Parameters,
): withMultisig.ReturnValue<transport> {
return ((options: Parameters<Transport>[0]) => {
const value = transport(options)
return {
...value,
request: Multisig.handleRequest(
(request, requestOptions) =>
value.request(request as never, requestOptions),
parameters,
) as typeof value.request,
value: { ...value.value, multisig: true },
}
}) as withMultisig.ReturnValue<transport>
}
export declare namespace withMultisig {
/** Multisig transport parameters. */
export type Parameters = Multisig.handleRequest.Parameters
/** Multisig transport return type. */
export type ReturnValue<transport extends Transport = Transport> =
transport extends Transport<
infer type,
infer rpcAttributes,
infer eip1193RequestFn
>
? Transport<
type,
rpcAttributes & {
/** Whether the transport coordinates native multisig approvals. */
multisig: true
},
eip1193RequestFn
>
: never
}
/**
* Creates a relay transport that routes requests between
* the default transport or the relay transport.
*
* All `eth_fillTransaction` requests are sent to the relay with the request's
* `feePayer` value preserved so the relay can decide whether to sponsor the transaction.
* Multisig approvals, configs, operations, and operation-aware transaction
* lookups are also sent to the relay so it can coordinate approvals in its store.
*
* The policy parameter controls how the relay handles sponsored transactions:
* - `'sign-only'`: Relay co-signs the transaction and returns it to the client transport, which then broadcasts it via the default transport
* - `'sign-and-broadcast'`: Relay co-signs and broadcasts the transaction directly
*
* @param defaultTransport - The default transport to use.
* @param relayTransport - The relay transport to use.
* @param parameters - Configuration parameters.
* @returns A relay transport.
*/
export function withRelay(
defaultTransport: Transport,
relayTransport: Transport,
parameters?: withRelay.Parameters,
): withRelay.ReturnValue {
const { policy = 'sign-only' } = parameters ?? {}
return (config) => {
const transport_default = defaultTransport(config)
const transport_relay = relayTransport(config)
const transport = createTransport({
key: withRelay.type,
name: 'Relay Proxy',
async request({ method, params }, options) {
if (method === 'eth_fillTransaction')
return transport_relay.request({ method, params }, options) as never
if (
method === 'eth_getTransactionByHash' ||
method === 'eth_getTransactionReceipt'
) {
const result = await transport_default.request(
{ method, params },
options,
)
if (result !== null && typeof result !== 'undefined')
return result as never
const operation = await (async () => {
try {
return await transport_relay.request(
{ method: 'multisig_getOperation', params },
options,
)
} catch (error) {
if (
error instanceof MethodNotFoundRpcError ||
error instanceof MethodNotSupportedRpcError
) {
// Relays created before multisig coordination do not expose this method.
return null
}
throw error
}
})()
if (
!operation ||
typeof operation !== 'object' ||
!('type' in operation) ||
operation.type !== 'transaction'
)
return result as never
return transport_relay.request({ method, params }, options) as never
}
if (
method === 'multisig_approveKeyAuthorization' ||
method === 'multisig_approveRawTransaction' ||
method === 'multisig_approveRawTransactionSync' ||
method === 'multisig_getConfig' ||
method === 'multisig_getOperation'
)
return transport_relay.request({ method, params }, options) as never
if (
method === 'eth_sendRawTransactionSync' ||
method === 'eth_sendRawTransaction'
) {
const serialized = (params as any)[0] as `0x76${string}`
const transaction = Transaction.deserialize(serialized)
if (transaction.signature?.type === 'multisig')
return transport_relay.request({ method, params }, options) as never
// Serialized Tempo envelopes encode `feePayer: true` as a missing fee payer
// signature until the relay co-signs the transaction.
if (transaction.feePayerSignature === null) {
// For 'sign-and-broadcast', relay signs and broadcasts
if (policy === 'sign-and-broadcast')
return transport_relay.request(
{ method, params },
options,
) as never
// For 'sign-only', request signature from relay using eth_signRawTransaction
{
// Request signature from relay using eth_signRawTransaction
const signedTransaction = await transport_relay.request(
{
method: 'eth_signRawTransaction',
params: [serialized],
},
options,
)
// Broadcast the signed transaction via the default transport
return transport_default.request(
{ method, params: [signedTransaction] },
options,
) as never
}
}
}
return (await transport_default.request(
{ method, params },
options,
)) as never
},
type: withRelay.type,
})
return { ...transport, value: { multisig: true } }
}
}
export declare namespace withRelay {
export const type = 'relay'
export type Parameters = RelayProxyParameters
export type ReturnValue = Relay
}
/** @deprecated Use `withRelay` instead. */
export function withFeePayer(
defaultTransport: Transport,
relayTransport: Transport,
parameters?: withFeePayer.Parameters,
): withFeePayer.ReturnValue {
const { policy = 'sign-only' } = parameters ?? {}
return (config) => {
const transport_default = defaultTransport(config)
const transport_relay = relayTransport(config)
return createTransport({
key: withFeePayer.type,
name: 'Relay Proxy',
async request({ method, params }, options) {
if (method === 'eth_fillTransaction') {
const request = (params as readonly unknown[] | undefined)?.[0]
if (
request &&
typeof request === 'object' &&
'feePayer' in request &&
request.feePayer === true
)
return transport_relay.request({ method, params }, options) as never
}
if (
method === 'eth_sendRawTransactionSync' ||
method === 'eth_sendRawTransaction'
) {
const serialized = (params as any)[0] as `0x76${string}`
const transaction = Transaction.deserialize(serialized)
// Serialized Tempo envelopes encode `feePayer: true` as a missing fee payer
// signature until the relay co-signs the transaction.
if (transaction.feePayerSignature === null) {
// For 'sign-and-broadcast', relay signs and broadcasts
if (policy === 'sign-and-broadcast')
return transport_relay.request(
{ method, params },
options,
) as never
// For 'sign-only', request signature from relay using eth_signRawTransaction
{
// Request signature from relay using eth_signRawTransaction
const signedTransaction = await transport_relay.request(
{
method: 'eth_signRawTransaction',
params: [serialized],
},
options,
)
// Broadcast the signed transaction via the default transport
return transport_default.request(
{ method, params: [signedTransaction] },
options,
) as never
}
}
}
return (await transport_default.request(
{ method, params },
options,
)) as never
},
type: withFeePayer.type,
})
}
}
export declare namespace withFeePayer {
export const type = 'feePayer'
export type Parameters = {
/** Policy for how the fee payer should handle transactions. Defaults to `'sign-only'`. */
policy?: 'sign-only' | 'sign-and-broadcast' | undefined
}
export type ReturnValue = FeePayer
}
/**
* Creates a transport that instruments a compatibility layer for
* `wallet_` RPC actions (`sendCalls`, `getCallsStatus`, etc).
*
* @param transport - Transport to wrap.
* @returns Transport.
*/
export function walletNamespaceCompat(
transport: Transport,
options: walletNamespaceCompat.Parameters,
): Transport {
const { account } = options
const sendCallsMagic = Hash.keccak256(Hex.fromString('TEMPO_5792'))
return (options) => {
const t = transport(options)
const chain = options.chain as Chain & ChainConfig
return {
...t,
async request(args: never) {
const request = RpcRequest.from(args)
const client = createClient({
chain,
transport,
})
if (request.method === 'wallet_sendCalls') {
const params = request.params[0] ?? {}
const { capabilities, chainId, from } = params
const { sync, ...properties } = capabilities ?? {}
if (!chainId) throw new Provider.UnsupportedChainIdError()
if (Number(chainId) !== client.chain.id)
throw new Provider.UnsupportedChainIdError()
if (from && !Address.isEqual(from, account.address))
throw new Provider.DisconnectedError()
const calls = (params.calls ?? []).map((call) => ({
to: call.to,
value: call.value ? BigInt(call.value) : undefined,
data: call.data,
}))
const hash = await (async () => {
if (!sync)
return sendTransaction(client, {
account,
...(properties ? properties : {}),
calls,
})
const { transactionHash } = await sendTransactionSync(client, {
account,
...(properties ? properties : {}),
calls,
})
return transactionHash
})()
const id = Hex.concat(hash, Hex.padLeft(chainId, 32), sendCallsMagic)
return {
capabilities: { sync },
id,
}
}
if (request.method === 'wallet_getCallsStatus') {
const [id] = request.params ?? []
if (!id) throw new Error('`id` not found')
if (!id.endsWith(sendCallsMagic.slice(2)))
throw new Error('`id` not supported')
Hex.assert(id)
const hash = Hex.slice(id, 0, 32)
const chainId = Hex.slice(id, 32, 64)
const receipt = await getTransactionReceipt(client, { hash })
return {
atomic: true,
chainId: Number(chainId),
id,
receipts: [receipt],
status: receipt.status === 'success' ? 200 : 500,
version: '2.0.0',
}
}
return t.request(args)
},
} as never
}
}
export declare namespace walletNamespaceCompat {
export type Parameters = {
account: LocalAccount
}
}