@swivy/viem 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/dist/packages/viem/src/index.d.ts +111 -0
- package/dist/packages/viem/src/index.js +485 -0
- package/package.json +42 -4
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @swivy/viem
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Swivy viem SDK. Testnet preview; production onboarding is gated on the Swop canary.
|
|
4
|
+
|
|
5
|
+
Public npm distribution is being prepared. Before first publication, use all SDK tarballs
|
|
6
|
+
from one operator-supplied release and verify its SHA256SUMS. After publication:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @swivy/viem@next
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Use an environment app ID and publishable key for browser/mobile apps. Keep environment
|
|
13
|
+
secret keys on the backend and deployment bootstrap credentials with the deployment operator.
|
|
14
|
+
Choose email or SMS in your environment policy; SMS recovery alone does not enable SMS login.
|
|
15
|
+
Never log login codes, runtime sessions or signing material.
|
|
16
|
+
|
|
17
|
+
Browser apps currently need a same-origin reverse proxy for `/v1/runtime/*`; the API
|
|
18
|
+
does not advertise cross-origin browser access. Backend and native clients can use the API directly.
|
|
19
|
+
|
|
20
|
+
Requires viem 2. Use the adapter with an authenticated Swivy runtime session and owner-approved signing policy.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { TransactionIntent } from "@swivy/core";
|
|
2
|
+
import { type Address, type LocalAccount, type SignableMessage, type TransactionSerializable, type TypedDataDefinition } from "viem";
|
|
3
|
+
export interface SwivyViemWaitOptions {
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
pollIntervalMs?: number;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The subset of `SwivyBrowserClient` (from `@swivy/web`) that `toSwivyViemAccount` needs.
|
|
10
|
+
* Any object with the same method works (a server-side wrapper, a test fake, ...).
|
|
11
|
+
*/
|
|
12
|
+
export interface SwivyViemSigningClient {
|
|
13
|
+
createSigningIntentAndWait(body: Record<string, unknown>, options?: SwivyViemWaitOptions): Promise<TransactionIntent>;
|
|
14
|
+
}
|
|
15
|
+
/** The subset of `SwivyBrowserClient` that `createSwivyEip1193Provider` needs. */
|
|
16
|
+
export interface SwivyViemClient extends SwivyViemSigningClient {
|
|
17
|
+
createTransactionIntentAndWait(body: Record<string, unknown>, options?: SwivyViemWaitOptions): Promise<TransactionIntent>;
|
|
18
|
+
}
|
|
19
|
+
/** A managed EVM account, e.g. a `ManagedAccountDescriptor` from `listEmbeddedWallets()`. */
|
|
20
|
+
export interface SwivyViemManagedAccount {
|
|
21
|
+
id: string;
|
|
22
|
+
address: string;
|
|
23
|
+
}
|
|
24
|
+
export type SwivySigningOperation = "signMessage" | "signTypedData" | "signTransaction" | "sendTransaction";
|
|
25
|
+
export interface SwivyChallengeContext {
|
|
26
|
+
operation: SwivySigningOperation;
|
|
27
|
+
accountId: string;
|
|
28
|
+
address: Address;
|
|
29
|
+
chainId?: number;
|
|
30
|
+
network?: string | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns a verified MFA `challengeId` (e.g. from `client.approveWithPasskey(...)`) when the
|
|
34
|
+
* environment's policy requires step-up, or `undefined`/`null` to send the intent without one.
|
|
35
|
+
*/
|
|
36
|
+
export type SwivyGetChallengeId = (context: SwivyChallengeContext) => Promise<string | null | undefined> | string | null | undefined;
|
|
37
|
+
export interface SwivyViemAccountOptions {
|
|
38
|
+
getChallengeId?: SwivyGetChallengeId;
|
|
39
|
+
waitOptions?: SwivyViemWaitOptions;
|
|
40
|
+
}
|
|
41
|
+
export declare class SwivyViemSigningError extends Error {
|
|
42
|
+
readonly intentId: string | null;
|
|
43
|
+
constructor(message: string, intentId?: string | null);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Convert a viem transaction (bigints, `gas`, string `type`) or an EIP-1193 transaction request
|
|
47
|
+
* (hex quantities, `gas`, `input`) into the JSON shape the Swivy API signs with ethers:
|
|
48
|
+
* numeric `type`/`chainId`/`nonce`, hex-string quantities, and `gasLimit`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function toSwivyTransactionPayload(transaction: Record<string, unknown> | TransactionSerializable): Record<string, unknown>;
|
|
51
|
+
/** Build the Swivy `typedData` payload from viem `signTypedData` parameters. */
|
|
52
|
+
export declare function toSwivyTypedDataPayload(typedData: TypedDataDefinition | Record<string, unknown>): Record<string, unknown>;
|
|
53
|
+
/** Build the Swivy message payload: `message` for UTF-8 strings, `messageHex` for raw bytes. */
|
|
54
|
+
export declare function toSwivyMessagePayload(message: SignableMessage): Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* Swivy equivalent of Privy's `toViemAccount({ wallet })`. Every signature goes through a Swivy
|
|
57
|
+
* signing intent (policy, MFA and audit apply); nothing signs a raw digest.
|
|
58
|
+
*/
|
|
59
|
+
export declare function toSwivyViemAccount(client: SwivyViemSigningClient, account: SwivyViemManagedAccount, options?: SwivyViemAccountOptions): LocalAccount;
|
|
60
|
+
export interface SwivyProviderChain {
|
|
61
|
+
id: number;
|
|
62
|
+
/** Swivy environment RPC target name, e.g. `base-mainnet`. Overrides the default map. */
|
|
63
|
+
network?: string;
|
|
64
|
+
/** Read-only JSON-RPC URL used for forwarded reads and transaction filling on this chain. */
|
|
65
|
+
rpcUrl?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface SwivyEip1193ProviderOptions extends SwivyViemAccountOptions {
|
|
68
|
+
/** Chains the provider may switch to. Accepts viem `Chain` objects too (only `id` is read). */
|
|
69
|
+
chains: SwivyProviderChain[];
|
|
70
|
+
/** Initial chain. Defaults to the first configured chain. */
|
|
71
|
+
initialChainId?: number;
|
|
72
|
+
/** chainId -> Swivy network name. Merged over `DEFAULT_SWIVY_EVM_NETWORKS`. */
|
|
73
|
+
networks?: Record<number, string>;
|
|
74
|
+
/** Fallback read-only RPC URL (or per-chain map) when a chain has no `rpcUrl`. */
|
|
75
|
+
rpcUrl?: string | Record<number, string>;
|
|
76
|
+
/** Fill nonce, gas and fees from the RPC before `eth_sendTransaction`. Default true. */
|
|
77
|
+
fillTransactions?: boolean;
|
|
78
|
+
/** Ask the API to sponsor gas for `eth_sendTransaction` (needs sponsorship configured). */
|
|
79
|
+
sponsorshipRequested?: boolean;
|
|
80
|
+
fetch?: typeof fetch;
|
|
81
|
+
}
|
|
82
|
+
/** Chain id to Swivy environment RPC target names (see `tools/rpc-presets.mjs`). */
|
|
83
|
+
export declare const DEFAULT_SWIVY_EVM_NETWORKS: Readonly<Record<number, string>>;
|
|
84
|
+
export interface SwivyEip1193RequestArguments {
|
|
85
|
+
method: string;
|
|
86
|
+
params?: readonly unknown[] | Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
type Listener = (...args: any[]) => void;
|
|
89
|
+
export interface SwivyEip1193Provider {
|
|
90
|
+
request(args: SwivyEip1193RequestArguments): Promise<any>;
|
|
91
|
+
on(event: string, listener: Listener): SwivyEip1193Provider;
|
|
92
|
+
removeListener(event: string, listener: Listener): SwivyEip1193Provider;
|
|
93
|
+
/** The viem account backing this provider (signs through Swivy intents). */
|
|
94
|
+
readonly account: LocalAccount;
|
|
95
|
+
readonly chainId: number;
|
|
96
|
+
/** Swap the managed account (e.g. after login) and emit `accountsChanged`. */
|
|
97
|
+
setAccount(account: SwivyViemManagedAccount | null): void;
|
|
98
|
+
/** Disconnect: emits `accountsChanged` with `[]` and `disconnect`. */
|
|
99
|
+
disconnect(): void;
|
|
100
|
+
}
|
|
101
|
+
export declare class SwivyProviderRpcError extends Error {
|
|
102
|
+
readonly code: number;
|
|
103
|
+
readonly data?: unknown;
|
|
104
|
+
constructor(code: number, message: string, data?: unknown);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* EIP-1193 provider backed by a Swivy managed EVM account. Plug it into wagmi with the built-in
|
|
108
|
+
* `injected({ target: () => ({ id: "swivy", name: "Swivy", provider }) })` connector.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createSwivyEip1193Provider(client: SwivyViemClient, account: SwivyViemManagedAccount, options: SwivyEip1193ProviderOptions): SwivyEip1193Provider;
|
|
111
|
+
export {};
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { getAddress, isAddressEqual, isHex, numberToHex, toHex, } from "viem";
|
|
2
|
+
import { toAccount } from "viem/accounts";
|
|
3
|
+
export class SwivyViemSigningError extends Error {
|
|
4
|
+
intentId;
|
|
5
|
+
constructor(message, intentId = null) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "SwivyViemSigningError";
|
|
8
|
+
this.intentId = intentId;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/* -------------------------------------------------------------------------------------------------
|
|
12
|
+
* Serialization helpers
|
|
13
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
14
|
+
const TRANSACTION_TYPE_NUMBERS = {
|
|
15
|
+
legacy: 0,
|
|
16
|
+
eip2930: 1,
|
|
17
|
+
eip1559: 2,
|
|
18
|
+
eip4844: 3,
|
|
19
|
+
eip7702: 4,
|
|
20
|
+
};
|
|
21
|
+
function quantityToHex(value) {
|
|
22
|
+
if (value === undefined || value === null) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "bigint" || typeof value === "number") {
|
|
26
|
+
return numberToHex(value);
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
return numberToHex(BigInt(value));
|
|
30
|
+
}
|
|
31
|
+
throw new SwivyViemSigningError("invalid_quantity");
|
|
32
|
+
}
|
|
33
|
+
function quantityToNumber(value) {
|
|
34
|
+
if (value === undefined || value === null) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "number") {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "bigint" || typeof value === "string") {
|
|
41
|
+
return Number(BigInt(value));
|
|
42
|
+
}
|
|
43
|
+
throw new SwivyViemSigningError("invalid_quantity");
|
|
44
|
+
}
|
|
45
|
+
function transactionTypeToNumber(value) {
|
|
46
|
+
if (value === undefined || value === null) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "number") {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
if (value in TRANSACTION_TYPE_NUMBERS) {
|
|
54
|
+
return TRANSACTION_TYPE_NUMBERS[value];
|
|
55
|
+
}
|
|
56
|
+
if (isHex(value) || /^\d+$/.test(value)) {
|
|
57
|
+
return Number(BigInt(value));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new SwivyViemSigningError("unsupported_transaction_type");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convert a viem transaction (bigints, `gas`, string `type`) or an EIP-1193 transaction request
|
|
64
|
+
* (hex quantities, `gas`, `input`) into the JSON shape the Swivy API signs with ethers:
|
|
65
|
+
* numeric `type`/`chainId`/`nonce`, hex-string quantities, and `gasLimit`.
|
|
66
|
+
*/
|
|
67
|
+
export function toSwivyTransactionPayload(transaction) {
|
|
68
|
+
const tx = transaction;
|
|
69
|
+
if (tx.blobs !== undefined || tx.sidecars !== undefined || tx.authorizationList !== undefined) {
|
|
70
|
+
throw new SwivyViemSigningError("unsupported_transaction_fields");
|
|
71
|
+
}
|
|
72
|
+
const payload = {
|
|
73
|
+
type: transactionTypeToNumber(tx.type),
|
|
74
|
+
chainId: quantityToNumber(tx.chainId),
|
|
75
|
+
nonce: quantityToNumber(tx.nonce),
|
|
76
|
+
to: tx.to ?? undefined,
|
|
77
|
+
value: quantityToHex(tx.value),
|
|
78
|
+
data: (tx.data ?? tx.input),
|
|
79
|
+
gasLimit: quantityToHex(tx.gas ?? tx.gasLimit),
|
|
80
|
+
gasPrice: quantityToHex(tx.gasPrice),
|
|
81
|
+
maxFeePerGas: quantityToHex(tx.maxFeePerGas),
|
|
82
|
+
maxPriorityFeePerGas: quantityToHex(tx.maxPriorityFeePerGas),
|
|
83
|
+
accessList: tx.accessList,
|
|
84
|
+
};
|
|
85
|
+
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
|
|
86
|
+
}
|
|
87
|
+
/** JSON-safe deep copy: bigints become decimal strings, byte arrays become hex. */
|
|
88
|
+
function toJsonSafe(value) {
|
|
89
|
+
if (typeof value === "bigint") {
|
|
90
|
+
return value.toString();
|
|
91
|
+
}
|
|
92
|
+
if (value instanceof Uint8Array) {
|
|
93
|
+
return toHex(value);
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return value.map(toJsonSafe);
|
|
97
|
+
}
|
|
98
|
+
if (value && typeof value === "object") {
|
|
99
|
+
return Object.fromEntries(Object.entries(value)
|
|
100
|
+
.filter(([, entry]) => entry !== undefined)
|
|
101
|
+
.map(([key, entry]) => [key, toJsonSafe(entry)]));
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
/** Build the Swivy `typedData` payload from viem `signTypedData` parameters. */
|
|
106
|
+
export function toSwivyTypedDataPayload(typedData) {
|
|
107
|
+
const definition = typedData;
|
|
108
|
+
return toJsonSafe({
|
|
109
|
+
domain: definition.domain ?? {},
|
|
110
|
+
types: definition.types,
|
|
111
|
+
primaryType: definition.primaryType ?? null,
|
|
112
|
+
message: definition.message ?? {},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Build the Swivy message payload: `message` for UTF-8 strings, `messageHex` for raw bytes. */
|
|
116
|
+
export function toSwivyMessagePayload(message) {
|
|
117
|
+
if (typeof message === "string") {
|
|
118
|
+
return { message };
|
|
119
|
+
}
|
|
120
|
+
const raw = message.raw;
|
|
121
|
+
return { messageHex: typeof raw === "string" ? raw : toHex(raw) };
|
|
122
|
+
}
|
|
123
|
+
function intentFailure(intent, fallback) {
|
|
124
|
+
return new SwivyViemSigningError(intent.result?.error ?? fallback, intent.id ?? null);
|
|
125
|
+
}
|
|
126
|
+
function requireSignature(intent) {
|
|
127
|
+
const signature = intent.result?.signature;
|
|
128
|
+
if (intent.status !== "completed" || !signature) {
|
|
129
|
+
throw intentFailure(intent, "swivy_signing_failed");
|
|
130
|
+
}
|
|
131
|
+
return signature;
|
|
132
|
+
}
|
|
133
|
+
async function resolveChallengeId(getChallengeId, context) {
|
|
134
|
+
if (!getChallengeId) {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
const challengeId = await getChallengeId(context);
|
|
138
|
+
return challengeId ? { challengeId } : {};
|
|
139
|
+
}
|
|
140
|
+
/* -------------------------------------------------------------------------------------------------
|
|
141
|
+
* toSwivyViemAccount
|
|
142
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
143
|
+
/**
|
|
144
|
+
* Swivy equivalent of Privy's `toViemAccount({ wallet })`. Every signature goes through a Swivy
|
|
145
|
+
* signing intent (policy, MFA and audit apply); nothing signs a raw digest.
|
|
146
|
+
*/
|
|
147
|
+
export function toSwivyViemAccount(client, account, options = {}) {
|
|
148
|
+
const address = getAddress(account.address);
|
|
149
|
+
async function sign(operation, payload, chainId) {
|
|
150
|
+
const challenge = await resolveChallengeId(options.getChallengeId, {
|
|
151
|
+
operation,
|
|
152
|
+
accountId: account.id,
|
|
153
|
+
address,
|
|
154
|
+
chainId,
|
|
155
|
+
});
|
|
156
|
+
const intent = await client.createSigningIntentAndWait({
|
|
157
|
+
accountId: account.id,
|
|
158
|
+
chain: "evm",
|
|
159
|
+
...challenge,
|
|
160
|
+
payload,
|
|
161
|
+
}, options.waitOptions);
|
|
162
|
+
return intent;
|
|
163
|
+
}
|
|
164
|
+
return toAccount({
|
|
165
|
+
address,
|
|
166
|
+
async signMessage({ message }) {
|
|
167
|
+
return requireSignature(await sign("signMessage", toSwivyMessagePayload(message)));
|
|
168
|
+
},
|
|
169
|
+
async signTypedData(typedData) {
|
|
170
|
+
const definition = typedData;
|
|
171
|
+
const domain = definition.domain;
|
|
172
|
+
return requireSignature(await sign("signTypedData", { typedData: toSwivyTypedDataPayload(definition) }, quantityToNumber(domain?.chainId)));
|
|
173
|
+
},
|
|
174
|
+
// viem passes `{ serializer }` for chains with custom formats (e.g. OP-stack deposit txs).
|
|
175
|
+
// Swivy signs standard EIP-155/2718 envelopes, which those chains accept for normal txs.
|
|
176
|
+
async signTransaction(transaction) {
|
|
177
|
+
const payload = toSwivyTransactionPayload(transaction);
|
|
178
|
+
const intent = await sign("signTransaction", { transaction: payload }, payload.chainId);
|
|
179
|
+
const signed = intent.result?.signedPayload ?? intent.result?.signature;
|
|
180
|
+
if (intent.status !== "completed" || !signed) {
|
|
181
|
+
throw intentFailure(intent, "swivy_signing_failed");
|
|
182
|
+
}
|
|
183
|
+
return signed;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** Chain id to Swivy environment RPC target names (see `tools/rpc-presets.mjs`). */
|
|
188
|
+
export const DEFAULT_SWIVY_EVM_NETWORKS = Object.freeze({
|
|
189
|
+
1: "ethereum-mainnet",
|
|
190
|
+
10: "optimism-mainnet",
|
|
191
|
+
137: "polygon-mainnet",
|
|
192
|
+
8453: "base-mainnet",
|
|
193
|
+
42161: "arbitrum-mainnet",
|
|
194
|
+
84532: "base-sepolia",
|
|
195
|
+
});
|
|
196
|
+
export class SwivyProviderRpcError extends Error {
|
|
197
|
+
code;
|
|
198
|
+
data;
|
|
199
|
+
constructor(code, message, data) {
|
|
200
|
+
super(message);
|
|
201
|
+
this.name = "SwivyProviderRpcError";
|
|
202
|
+
this.code = code;
|
|
203
|
+
this.data = data;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const UNSUPPORTED_SIGNING_METHODS = new Set([
|
|
207
|
+
"eth_sign",
|
|
208
|
+
"eth_signTypedData",
|
|
209
|
+
"eth_signTypedData_v1",
|
|
210
|
+
"eth_signTypedData_v3",
|
|
211
|
+
"wallet_addEthereumChain",
|
|
212
|
+
"wallet_watchAsset",
|
|
213
|
+
"wallet_sendCalls",
|
|
214
|
+
]);
|
|
215
|
+
function paramsArray(params) {
|
|
216
|
+
if (params === undefined) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
return Array.isArray(params) ? [...params] : [params];
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* EIP-1193 provider backed by a Swivy managed EVM account. Plug it into wagmi with the built-in
|
|
223
|
+
* `injected({ target: () => ({ id: "swivy", name: "Swivy", provider }) })` connector.
|
|
224
|
+
*/
|
|
225
|
+
export function createSwivyEip1193Provider(client, account, options) {
|
|
226
|
+
if (!options.chains?.length) {
|
|
227
|
+
throw new Error("swivy_provider_requires_chains");
|
|
228
|
+
}
|
|
229
|
+
const chains = new Map(options.chains.map((chain) => [Number(chain.id), chain]));
|
|
230
|
+
const networks = { ...DEFAULT_SWIVY_EVM_NETWORKS, ...(options.networks ?? {}) };
|
|
231
|
+
const fetchFn = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
232
|
+
const listeners = new Map();
|
|
233
|
+
let currentChainId = options.initialChainId ?? Number(options.chains[0].id);
|
|
234
|
+
if (!chains.has(currentChainId)) {
|
|
235
|
+
throw new Error("swivy_provider_initial_chain_not_configured");
|
|
236
|
+
}
|
|
237
|
+
let managedAccount = account;
|
|
238
|
+
let viemAccount = toSwivyViemAccount(client, account, options);
|
|
239
|
+
let rpcId = 0;
|
|
240
|
+
let connected = false;
|
|
241
|
+
function emit(event, ...args) {
|
|
242
|
+
for (const listener of listeners.get(event) ?? []) {
|
|
243
|
+
try {
|
|
244
|
+
listener(...args);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Listener errors must not break the provider.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function networkFor(chainId) {
|
|
252
|
+
return chains.get(chainId)?.network ?? networks[chainId] ?? null;
|
|
253
|
+
}
|
|
254
|
+
function rpcUrlFor(chainId) {
|
|
255
|
+
const chain = chains.get(chainId);
|
|
256
|
+
if (chain?.rpcUrl) {
|
|
257
|
+
return chain.rpcUrl;
|
|
258
|
+
}
|
|
259
|
+
if (typeof options.rpcUrl === "string") {
|
|
260
|
+
return options.rpcUrl;
|
|
261
|
+
}
|
|
262
|
+
return options.rpcUrl?.[chainId] ?? null;
|
|
263
|
+
}
|
|
264
|
+
function requireAccount() {
|
|
265
|
+
if (!managedAccount) {
|
|
266
|
+
throw new SwivyProviderRpcError(4100, "Swivy account is not connected");
|
|
267
|
+
}
|
|
268
|
+
return managedAccount;
|
|
269
|
+
}
|
|
270
|
+
function assertOwnAddress(candidate) {
|
|
271
|
+
const own = requireAccount();
|
|
272
|
+
if (typeof candidate === "string" &&
|
|
273
|
+
candidate &&
|
|
274
|
+
!isAddressEqual(candidate, own.address)) {
|
|
275
|
+
throw new SwivyProviderRpcError(4100, "Requested address is not the connected Swivy account");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function rpc(method, params) {
|
|
279
|
+
const url = rpcUrlFor(currentChainId);
|
|
280
|
+
if (!url) {
|
|
281
|
+
throw new SwivyProviderRpcError(4200, `Method not supported: ${method}`);
|
|
282
|
+
}
|
|
283
|
+
rpcId += 1;
|
|
284
|
+
const response = await fetchFn(url, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: { "content-type": "application/json" },
|
|
287
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: rpcId, method, params }),
|
|
288
|
+
});
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
throw new SwivyProviderRpcError(-32603, `RPC request failed with status ${response.status}`);
|
|
291
|
+
}
|
|
292
|
+
const body = (await response.json());
|
|
293
|
+
if (body.error) {
|
|
294
|
+
throw new SwivyProviderRpcError(body.error.code ?? -32603, body.error.message ?? "RPC error", body.error.data);
|
|
295
|
+
}
|
|
296
|
+
return body.result;
|
|
297
|
+
}
|
|
298
|
+
async function fillTransaction(request, from) {
|
|
299
|
+
const tx = { ...request, chainId: currentChainId };
|
|
300
|
+
if (options.fillTransactions === false || !rpcUrlFor(currentChainId)) {
|
|
301
|
+
return tx;
|
|
302
|
+
}
|
|
303
|
+
if (tx.nonce === undefined) {
|
|
304
|
+
tx.nonce = await rpc("eth_getTransactionCount", [from, "pending"]);
|
|
305
|
+
}
|
|
306
|
+
if (tx.gas === undefined && tx.gasLimit === undefined) {
|
|
307
|
+
const { chainId: _chainId, nonce: _nonce, type: _type, ...estimateRequest } = tx;
|
|
308
|
+
tx.gas = await rpc("eth_estimateGas", [{ ...estimateRequest, from }]);
|
|
309
|
+
}
|
|
310
|
+
const hasFees = tx.gasPrice !== undefined || (tx.maxFeePerGas !== undefined && tx.maxPriorityFeePerGas !== undefined);
|
|
311
|
+
if (!hasFees) {
|
|
312
|
+
const block = (await rpc("eth_getBlockByNumber", ["latest", false]));
|
|
313
|
+
if (block?.baseFeePerGas && tx.type !== "0x0" && tx.type !== "legacy") {
|
|
314
|
+
const priority = tx.maxPriorityFeePerGas !== undefined
|
|
315
|
+
? BigInt(tx.maxPriorityFeePerGas)
|
|
316
|
+
: BigInt((await rpc("eth_maxPriorityFeePerGas", [])));
|
|
317
|
+
tx.maxPriorityFeePerGas = numberToHex(priority);
|
|
318
|
+
tx.maxFeePerGas = tx.maxFeePerGas ?? numberToHex(BigInt(block.baseFeePerGas) * 2n + priority);
|
|
319
|
+
tx.type = tx.type ?? "0x2";
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
tx.gasPrice = await rpc("eth_gasPrice", []);
|
|
323
|
+
tx.type = tx.type ?? "0x0";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return tx;
|
|
327
|
+
}
|
|
328
|
+
async function sendTransaction(params) {
|
|
329
|
+
const own = requireAccount();
|
|
330
|
+
const request = { ...(params[0] ?? {}) };
|
|
331
|
+
assertOwnAddress(request.from);
|
|
332
|
+
if (request.chainId !== undefined && Number(BigInt(request.chainId)) !== currentChainId) {
|
|
333
|
+
throw new SwivyProviderRpcError(4901, "Transaction chainId does not match the active chain");
|
|
334
|
+
}
|
|
335
|
+
delete request.from;
|
|
336
|
+
const filled = await fillTransaction(request, own.address);
|
|
337
|
+
const transaction = toSwivyTransactionPayload(filled);
|
|
338
|
+
const network = networkFor(currentChainId);
|
|
339
|
+
const challenge = await resolveChallengeId(options.getChallengeId, {
|
|
340
|
+
operation: "sendTransaction",
|
|
341
|
+
accountId: own.id,
|
|
342
|
+
address: getAddress(own.address),
|
|
343
|
+
chainId: currentChainId,
|
|
344
|
+
network,
|
|
345
|
+
});
|
|
346
|
+
const intent = await client.createTransactionIntentAndWait({
|
|
347
|
+
accountId: own.id,
|
|
348
|
+
chain: "evm",
|
|
349
|
+
...challenge,
|
|
350
|
+
payload: {
|
|
351
|
+
...(network ? { network } : {}),
|
|
352
|
+
...(options.sponsorshipRequested ? { sponsorshipRequested: true } : {}),
|
|
353
|
+
transaction,
|
|
354
|
+
},
|
|
355
|
+
}, options.waitOptions);
|
|
356
|
+
const hash = intent.result?.transactionHash;
|
|
357
|
+
if (intent.status !== "completed" || !hash) {
|
|
358
|
+
throw new SwivyProviderRpcError(-32603, intent.result?.error ?? "swivy_transaction_failed", {
|
|
359
|
+
intentId: intent.id,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return hash;
|
|
363
|
+
}
|
|
364
|
+
function parseTypedData(raw) {
|
|
365
|
+
if (typeof raw === "string") {
|
|
366
|
+
try {
|
|
367
|
+
return JSON.parse(raw);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
throw new SwivyProviderRpcError(-32602, "Invalid typed data JSON");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (raw && typeof raw === "object") {
|
|
374
|
+
return raw;
|
|
375
|
+
}
|
|
376
|
+
throw new SwivyProviderRpcError(-32602, "Missing typed data");
|
|
377
|
+
}
|
|
378
|
+
async function request({ method, params }) {
|
|
379
|
+
const args = paramsArray(params);
|
|
380
|
+
switch (method) {
|
|
381
|
+
case "eth_requestAccounts":
|
|
382
|
+
case "eth_accounts": {
|
|
383
|
+
if (!managedAccount) {
|
|
384
|
+
if (method === "eth_requestAccounts") {
|
|
385
|
+
throw new SwivyProviderRpcError(4100, "Swivy account is not connected");
|
|
386
|
+
}
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
const accounts = [getAddress(managedAccount.address)];
|
|
390
|
+
if (method === "eth_requestAccounts" && !connected) {
|
|
391
|
+
connected = true;
|
|
392
|
+
emit("connect", { chainId: numberToHex(currentChainId) });
|
|
393
|
+
}
|
|
394
|
+
return accounts;
|
|
395
|
+
}
|
|
396
|
+
case "eth_chainId":
|
|
397
|
+
return numberToHex(currentChainId);
|
|
398
|
+
case "net_version":
|
|
399
|
+
return String(currentChainId);
|
|
400
|
+
case "wallet_switchEthereumChain": {
|
|
401
|
+
const target = (args[0] ?? {});
|
|
402
|
+
if (!target.chainId) {
|
|
403
|
+
throw new SwivyProviderRpcError(-32602, "Missing chainId");
|
|
404
|
+
}
|
|
405
|
+
const nextChainId = Number(BigInt(target.chainId));
|
|
406
|
+
if (!chains.has(nextChainId)) {
|
|
407
|
+
throw new SwivyProviderRpcError(4902, `Unrecognized chain ID ${target.chainId}`);
|
|
408
|
+
}
|
|
409
|
+
if (nextChainId !== currentChainId) {
|
|
410
|
+
currentChainId = nextChainId;
|
|
411
|
+
emit("chainChanged", numberToHex(nextChainId));
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
case "wallet_requestPermissions":
|
|
416
|
+
case "wallet_getPermissions":
|
|
417
|
+
return managedAccount ? [{ parentCapability: "eth_accounts" }] : [];
|
|
418
|
+
case "wallet_revokePermissions":
|
|
419
|
+
return null;
|
|
420
|
+
case "personal_sign": {
|
|
421
|
+
const [data, address] = args;
|
|
422
|
+
assertOwnAddress(address);
|
|
423
|
+
if (typeof data !== "string") {
|
|
424
|
+
throw new SwivyProviderRpcError(-32602, "Missing message");
|
|
425
|
+
}
|
|
426
|
+
return viemAccount.signMessage({
|
|
427
|
+
message: isHex(data) ? { raw: data } : data,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
case "eth_signTypedData_v4": {
|
|
431
|
+
const [address, typedData] = args;
|
|
432
|
+
assertOwnAddress(address);
|
|
433
|
+
return viemAccount.signTypedData(parseTypedData(typedData));
|
|
434
|
+
}
|
|
435
|
+
case "eth_signTransaction": {
|
|
436
|
+
const tx = { ...(args[0] ?? {}) };
|
|
437
|
+
assertOwnAddress(tx.from);
|
|
438
|
+
delete tx.from;
|
|
439
|
+
const filled = await fillTransaction(tx, requireAccount().address);
|
|
440
|
+
return viemAccount.signTransaction(toSwivyTransactionPayload(filled));
|
|
441
|
+
}
|
|
442
|
+
case "eth_sendTransaction":
|
|
443
|
+
return sendTransaction(args);
|
|
444
|
+
default:
|
|
445
|
+
if (UNSUPPORTED_SIGNING_METHODS.has(method)) {
|
|
446
|
+
throw new SwivyProviderRpcError(4200, `Method not supported: ${method}`);
|
|
447
|
+
}
|
|
448
|
+
return rpc(method, args);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const provider = {
|
|
452
|
+
request,
|
|
453
|
+
on(event, listener) {
|
|
454
|
+
if (!listeners.has(event)) {
|
|
455
|
+
listeners.set(event, new Set());
|
|
456
|
+
}
|
|
457
|
+
listeners.get(event).add(listener);
|
|
458
|
+
return provider;
|
|
459
|
+
},
|
|
460
|
+
removeListener(event, listener) {
|
|
461
|
+
listeners.get(event)?.delete(listener);
|
|
462
|
+
return provider;
|
|
463
|
+
},
|
|
464
|
+
get account() {
|
|
465
|
+
return viemAccount;
|
|
466
|
+
},
|
|
467
|
+
get chainId() {
|
|
468
|
+
return currentChainId;
|
|
469
|
+
},
|
|
470
|
+
setAccount(next) {
|
|
471
|
+
managedAccount = next;
|
|
472
|
+
if (next) {
|
|
473
|
+
viemAccount = toSwivyViemAccount(client, next, options);
|
|
474
|
+
}
|
|
475
|
+
emit("accountsChanged", next ? [getAddress(next.address)] : []);
|
|
476
|
+
},
|
|
477
|
+
disconnect() {
|
|
478
|
+
managedAccount = null;
|
|
479
|
+
connected = false;
|
|
480
|
+
emit("accountsChanged", []);
|
|
481
|
+
emit("disconnect", new SwivyProviderRpcError(4900, "Swivy provider disconnected"));
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
return provider;
|
|
485
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swivy/viem",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Swivy viem account and EIP-1193 provider (drop-in for Privy's toViemAccount and @privy-io/wagmi)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/packages/viem/src/index.js",
|
|
7
|
+
"types": "dist/packages/viem/src/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run test/*.test.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@swivy/core": "^1.0.0"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"viem": "^2.46.3"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"ethers": "^6.17.0",
|
|
20
|
+
"viem": "^2.46.3"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/packages/viem/src"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/packages/viem/src/index.d.ts",
|
|
28
|
+
"import": "./dist/packages/viem/src/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
},
|
|
38
|
+
"license": "ISC",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/Travisswop/Swivy.git",
|
|
42
|
+
"directory": "packages/viem"
|
|
43
|
+
}
|
|
44
|
+
}
|