@aixyz/cli 0.7.0 → 0.9.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 +1 -1
- package/bin.ts +137 -0
- package/build/AixyzConfigPlugin.ts +1 -1
- package/build/AixyzServerPlugin.ts +43 -9
- package/build/index.ts +1 -1
- package/dev/index.ts +1 -1
- package/package.json +11 -6
- package/register/index.ts +8 -0
- package/register/register.test.ts +66 -0
- package/register/register.ts +204 -0
- package/register/update.test.ts +47 -0
- package/register/update.ts +191 -0
- package/register/utils/chain.ts +56 -0
- package/register/utils/erc8004-file.ts +65 -0
- package/register/utils/prompt.ts +78 -0
- package/register/utils/result.ts +14 -0
- package/register/utils/spinner.ts +39 -0
- package/register/utils/transaction.ts +94 -0
- package/register/utils.test.ts +81 -0
- package/register/utils.ts +38 -0
- package/register/wallet/browser.test.ts +124 -0
- package/register/wallet/browser.ts +753 -0
- package/register/wallet/index.test.ts +80 -0
- package/register/wallet/index.ts +83 -0
- package/register/wallet/keystore.test.ts +63 -0
- package/register/wallet/keystore.ts +32 -0
- package/register/wallet/privatekey.test.ts +30 -0
- package/register/wallet/privatekey.ts +19 -0
- package/register/wallet/sign.test.ts +78 -0
- package/register/wallet/sign.ts +112 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { sepolia } from "viem/chains";
|
|
3
|
+
import { selectWalletMethod, createWalletFromMethod, type WalletOptions } from "./index";
|
|
4
|
+
|
|
5
|
+
// Valid test private key (do not use in production!)
|
|
6
|
+
const TEST_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
|
7
|
+
|
|
8
|
+
describe("selectWalletMethod", () => {
|
|
9
|
+
let originalEnv: string | undefined;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
originalEnv = process.env.PRIVATE_KEY;
|
|
13
|
+
delete process.env.PRIVATE_KEY;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (originalEnv !== undefined) {
|
|
18
|
+
process.env.PRIVATE_KEY = originalEnv;
|
|
19
|
+
} else {
|
|
20
|
+
delete process.env.PRIVATE_KEY;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns keystore method when keystore option provided", async () => {
|
|
25
|
+
const options: WalletOptions = { keystore: "/path/to/keystore" };
|
|
26
|
+
const result = await selectWalletMethod(options);
|
|
27
|
+
expect(result).toEqual({ type: "keystore", path: "/path/to/keystore" });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("returns browser method when browser option provided", async () => {
|
|
31
|
+
const options: WalletOptions = { browser: true };
|
|
32
|
+
const result = await selectWalletMethod(options);
|
|
33
|
+
expect(result).toEqual({ type: "browser" });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns privatekey method when PRIVATE_KEY env var set", async () => {
|
|
37
|
+
process.env.PRIVATE_KEY = TEST_PRIVATE_KEY;
|
|
38
|
+
const options: WalletOptions = {};
|
|
39
|
+
const result = await selectWalletMethod(options);
|
|
40
|
+
expect(result.type).toBe("privatekey");
|
|
41
|
+
expect(result.type === "privatekey" && (await result.resolveKey())).toBe(TEST_PRIVATE_KEY);
|
|
42
|
+
expect(process.env.PRIVATE_KEY).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("keystore option takes precedence over env var", async () => {
|
|
46
|
+
process.env.PRIVATE_KEY = TEST_PRIVATE_KEY;
|
|
47
|
+
const options: WalletOptions = { keystore: "/path/to/keystore" };
|
|
48
|
+
const result = await selectWalletMethod(options);
|
|
49
|
+
expect(result).toEqual({ type: "keystore", path: "/path/to/keystore" });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("browser option takes precedence over env var", async () => {
|
|
53
|
+
process.env.PRIVATE_KEY = TEST_PRIVATE_KEY;
|
|
54
|
+
const options: WalletOptions = { browser: true };
|
|
55
|
+
const result = await selectWalletMethod(options);
|
|
56
|
+
expect(result).toEqual({ type: "browser" });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("createWalletFromMethod", () => {
|
|
61
|
+
test("creates wallet for privatekey method", async () => {
|
|
62
|
+
const method = { type: "privatekey" as const, resolveKey: () => Promise.resolve(TEST_PRIVATE_KEY) };
|
|
63
|
+
const wallet = await createWalletFromMethod(method, sepolia);
|
|
64
|
+
expect(wallet).toBeDefined();
|
|
65
|
+
expect(wallet.account).toBeDefined();
|
|
66
|
+
expect(wallet.account?.address).toStrictEqual("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("throws for missing keystore file", async () => {
|
|
70
|
+
const method = { type: "keystore" as const, path: "/path/to/keystore" };
|
|
71
|
+
await expect(createWalletFromMethod(method, sepolia)).rejects.toThrow("Keystore file not found");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("throws for browser method (should use registerWithBrowser)", async () => {
|
|
75
|
+
const method = { type: "browser" as const };
|
|
76
|
+
await expect(createWalletFromMethod(method, sepolia)).rejects.toThrow(
|
|
77
|
+
"Browser wallets should use registerWithBrowser",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import type { Chain, WalletClient } from "viem";
|
|
3
|
+
import { select, input, password } from "@inquirer/prompts";
|
|
4
|
+
import { createPrivateKeyWallet } from "./privatekey";
|
|
5
|
+
import { createKeystoreWallet } from "./keystore";
|
|
6
|
+
|
|
7
|
+
export interface WalletOptions {
|
|
8
|
+
keystore?: string;
|
|
9
|
+
browser?: boolean;
|
|
10
|
+
broadcast?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type WalletMethod =
|
|
14
|
+
| { type: "keystore"; path: string }
|
|
15
|
+
| { type: "browser" }
|
|
16
|
+
| { type: "privatekey"; resolveKey: () => Promise<string> };
|
|
17
|
+
|
|
18
|
+
export async function selectWalletMethod(options: WalletOptions): Promise<WalletMethod> {
|
|
19
|
+
// Check explicit options first
|
|
20
|
+
if (options.keystore) {
|
|
21
|
+
return { type: "keystore", path: options.keystore };
|
|
22
|
+
}
|
|
23
|
+
if (options.browser) {
|
|
24
|
+
return { type: "browser" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check for PRIVATE_KEY environment variable
|
|
28
|
+
const envPrivateKey = process.env.PRIVATE_KEY;
|
|
29
|
+
if (envPrivateKey) {
|
|
30
|
+
delete process.env.PRIVATE_KEY;
|
|
31
|
+
console.warn("Warning: Using PRIVATE_KEY from environment variable");
|
|
32
|
+
return { type: "privatekey", resolveKey: () => Promise.resolve(envPrivateKey) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Interactive: prompt user to choose
|
|
36
|
+
const method = await select({
|
|
37
|
+
message: "Select signing method:",
|
|
38
|
+
choices: [
|
|
39
|
+
{ name: "Keystore file", value: "keystore" },
|
|
40
|
+
{ name: "Browser wallet (any EIP-6963 compatible wallets)", value: "browser" },
|
|
41
|
+
{ name: "Private key (not recommended)", value: "privatekey" },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
switch (method) {
|
|
46
|
+
case "keystore": {
|
|
47
|
+
const keystorePath = await input({
|
|
48
|
+
message: "Enter keystore path:",
|
|
49
|
+
default: `${homedir()}/.foundry/keystores/default`,
|
|
50
|
+
});
|
|
51
|
+
return { type: "keystore", path: keystorePath };
|
|
52
|
+
}
|
|
53
|
+
case "browser":
|
|
54
|
+
return { type: "browser" };
|
|
55
|
+
case "privatekey": {
|
|
56
|
+
const key = await password({
|
|
57
|
+
message: "Enter private key:",
|
|
58
|
+
mask: "*",
|
|
59
|
+
});
|
|
60
|
+
console.warn("Warning: Using raw private key is not recommended for production");
|
|
61
|
+
return { type: "privatekey", resolveKey: () => Promise.resolve(key) };
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
throw new Error("No wallet method selected");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function createWalletFromMethod(
|
|
69
|
+
method: WalletMethod,
|
|
70
|
+
chain: Chain,
|
|
71
|
+
rpcUrl?: string,
|
|
72
|
+
): Promise<WalletClient> {
|
|
73
|
+
switch (method.type) {
|
|
74
|
+
case "privatekey":
|
|
75
|
+
return createPrivateKeyWallet(await method.resolveKey(), chain, rpcUrl);
|
|
76
|
+
case "keystore":
|
|
77
|
+
return createKeystoreWallet(method.path, chain, rpcUrl);
|
|
78
|
+
case "browser":
|
|
79
|
+
throw new Error("Browser wallets should use registerWithBrowser, not createWalletFromMethod");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { createPrivateKeyWallet } from "./privatekey";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test, mock, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { encryptKeystoreJsonSync } from "ethers";
|
|
3
|
+
import { sepolia } from "viem/chains";
|
|
4
|
+
import { mkdirSync, writeFileSync, rmSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { tmpdir } from "os";
|
|
7
|
+
|
|
8
|
+
const TEST_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
|
9
|
+
const TEST_ADDRESS = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf";
|
|
10
|
+
const TEST_PASSWORD = "testpassword";
|
|
11
|
+
|
|
12
|
+
const testDir = join(tmpdir(), "aixyz-cli-keystore-test");
|
|
13
|
+
const testKeystorePath = join(testDir, "test-keystore.json");
|
|
14
|
+
|
|
15
|
+
// Mock the password prompt to return TEST_PASSWORD
|
|
16
|
+
mock.module("@inquirer/prompts", () => ({
|
|
17
|
+
password: () => Promise.resolve(TEST_PASSWORD),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Import after mocking
|
|
21
|
+
const { decryptKeystore, createKeystoreWallet } = await import("./keystore.js");
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
const json = encryptKeystoreJsonSync({ address: TEST_ADDRESS, privateKey: TEST_PRIVATE_KEY }, TEST_PASSWORD, {
|
|
25
|
+
scrypt: { N: 2, r: 1, p: 1 },
|
|
26
|
+
});
|
|
27
|
+
mkdirSync(testDir, { recursive: true });
|
|
28
|
+
writeFileSync(testKeystorePath, json);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("decryptKeystore", () => {
|
|
36
|
+
test("returns private key from keystore file", async () => {
|
|
37
|
+
const key = await decryptKeystore(testKeystorePath);
|
|
38
|
+
expect(key).toBe(TEST_PRIVATE_KEY);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("throws for missing file", async () => {
|
|
42
|
+
await expect(decryptKeystore("/nonexistent/keystore.json")).rejects.toThrow("Keystore file not found");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("throws for invalid keystore JSON", async () => {
|
|
46
|
+
const invalidPath = join(testDir, "invalid-keystore.json");
|
|
47
|
+
writeFileSync(invalidPath, JSON.stringify({ not: "a keystore" }));
|
|
48
|
+
await expect(decryptKeystore(invalidPath)).rejects.toThrow("Invalid keystore file");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("createKeystoreWallet", () => {
|
|
53
|
+
test("creates wallet with correct account", async () => {
|
|
54
|
+
const wallet = await createKeystoreWallet(testKeystorePath, sepolia);
|
|
55
|
+
expect(wallet.account).toBeDefined();
|
|
56
|
+
expect(wallet.account?.address).toBe(TEST_ADDRESS);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("creates wallet with chain configured", async () => {
|
|
60
|
+
const wallet = await createKeystoreWallet(testKeystorePath, sepolia);
|
|
61
|
+
expect(wallet.chain).toStrictEqual(sepolia);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// ethers is used solely for V3 keystore decryption — viem does not support this.
|
|
2
|
+
import { decryptKeystoreJson, isKeystoreJson } from "ethers";
|
|
3
|
+
import { createWalletClient, http, type Chain, type WalletClient } from "viem";
|
|
4
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
5
|
+
import { password } from "@inquirer/prompts";
|
|
6
|
+
|
|
7
|
+
export async function decryptKeystore(keystorePath: string): Promise<`0x${string}`> {
|
|
8
|
+
const file = Bun.file(keystorePath);
|
|
9
|
+
if (!(await file.exists())) {
|
|
10
|
+
throw new Error(`Keystore file not found: ${keystorePath}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const json = await file.text();
|
|
14
|
+
if (!isKeystoreJson(json)) {
|
|
15
|
+
throw new Error(`Invalid keystore file: ${keystorePath}`);
|
|
16
|
+
}
|
|
17
|
+
const pass = await password({ message: "Enter keystore password:", mask: "*" });
|
|
18
|
+
const account = await decryptKeystoreJson(json, pass);
|
|
19
|
+
|
|
20
|
+
return account.privateKey as `0x${string}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function createKeystoreWallet(keystorePath: string, chain: Chain, rpcUrl?: string): Promise<WalletClient> {
|
|
24
|
+
const privateKey = await decryptKeystore(keystorePath);
|
|
25
|
+
const account = privateKeyToAccount(privateKey);
|
|
26
|
+
|
|
27
|
+
return createWalletClient({
|
|
28
|
+
account,
|
|
29
|
+
chain,
|
|
30
|
+
transport: http(rpcUrl),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { sepolia } from "viem/chains";
|
|
3
|
+
import { createPrivateKeyWallet } from "./privatekey";
|
|
4
|
+
|
|
5
|
+
// Valid test private key (do not use in production!)
|
|
6
|
+
const TEST_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
|
7
|
+
|
|
8
|
+
describe("createPrivateKeyWallet", () => {
|
|
9
|
+
test("creates wallet with correct address", () => {
|
|
10
|
+
const wallet = createPrivateKeyWallet(TEST_PRIVATE_KEY, sepolia);
|
|
11
|
+
expect(wallet.account).toBeDefined();
|
|
12
|
+
// Private key 1 corresponds to this address
|
|
13
|
+
expect(wallet.account?.address).toStrictEqual("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("creates wallet with chain configured", () => {
|
|
17
|
+
const wallet = createPrivateKeyWallet(TEST_PRIVATE_KEY, sepolia);
|
|
18
|
+
expect(wallet.chain).toStrictEqual(sepolia);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("throws for invalid private key", () => {
|
|
22
|
+
expect(() => createPrivateKeyWallet("invalid", sepolia)).toThrow("Invalid private key format");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("accepts key without 0x prefix", () => {
|
|
26
|
+
const keyWithoutPrefix = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
27
|
+
const wallet = createPrivateKeyWallet(keyWithoutPrefix, sepolia);
|
|
28
|
+
expect(wallet.account?.address).toStrictEqual("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createWalletClient, http, type Chain, type WalletClient } from "viem";
|
|
2
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
|
|
4
|
+
export function createPrivateKeyWallet(privateKey: string, chain: Chain, rpcUrl?: string): WalletClient {
|
|
5
|
+
const key = (privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as `0x${string}`;
|
|
6
|
+
|
|
7
|
+
let account;
|
|
8
|
+
try {
|
|
9
|
+
account = privateKeyToAccount(key);
|
|
10
|
+
} catch {
|
|
11
|
+
throw new Error("Invalid private key format. Expected 64 hex characters (with or without 0x prefix).");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return createWalletClient({
|
|
15
|
+
account,
|
|
16
|
+
chain,
|
|
17
|
+
transport: http(rpcUrl),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test, mock } from "bun:test";
|
|
2
|
+
import { sepolia } from "viem/chains";
|
|
3
|
+
|
|
4
|
+
const FAKE_TX_HASH = "0x" + "ab".repeat(32);
|
|
5
|
+
const FAKE_RAW = "0x02deadbeef" as `0x${string}`;
|
|
6
|
+
const FAKE_ADDRESS = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf" as `0x${string}`;
|
|
7
|
+
|
|
8
|
+
mock.module("./browser.js", () => ({
|
|
9
|
+
signWithBrowser: () => Promise.resolve({ txHash: FAKE_TX_HASH }),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
mock.module("./index.js", () => ({
|
|
13
|
+
createWalletFromMethod: () =>
|
|
14
|
+
Promise.resolve({
|
|
15
|
+
account: { address: FAKE_ADDRESS },
|
|
16
|
+
prepareTransactionRequest: () => Promise.resolve({ to: "0x1234", data: "0x" }),
|
|
17
|
+
signTransaction: () => Promise.resolve(FAKE_RAW),
|
|
18
|
+
}),
|
|
19
|
+
selectWalletMethod: () => Promise.resolve({ type: "browser" }),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const { signTransaction } = await import("./sign.js");
|
|
23
|
+
|
|
24
|
+
const baseTx = {
|
|
25
|
+
to: "0x1234567890abcdef1234567890abcdef12345678" as `0x${string}`,
|
|
26
|
+
data: "0xdeadbeef" as `0x${string}`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("signTransaction", () => {
|
|
30
|
+
test("browser method routes to signViaBrowser and returns sent result", async () => {
|
|
31
|
+
const result = await signTransaction({
|
|
32
|
+
walletMethod: { type: "browser" },
|
|
33
|
+
tx: baseTx,
|
|
34
|
+
chain: sepolia,
|
|
35
|
+
options: { browser: { chainId: 11155111, chainName: "sepolia" } },
|
|
36
|
+
});
|
|
37
|
+
expect(result.kind).toBe("sent");
|
|
38
|
+
if (result.kind === "sent") {
|
|
39
|
+
expect(result.txHash).toBe(FAKE_TX_HASH);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("browser method throws without browser options", async () => {
|
|
44
|
+
await expect(
|
|
45
|
+
signTransaction({
|
|
46
|
+
walletMethod: { type: "browser" },
|
|
47
|
+
tx: baseTx,
|
|
48
|
+
chain: sepolia,
|
|
49
|
+
}),
|
|
50
|
+
).rejects.toThrow("Browser wallet requires chainId and chainName parameters");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("privatekey method routes to wallet client and returns signed result", async () => {
|
|
54
|
+
const result = await signTransaction({
|
|
55
|
+
walletMethod: { type: "privatekey", resolveKey: () => Promise.resolve("0x01") },
|
|
56
|
+
tx: baseTx,
|
|
57
|
+
chain: sepolia,
|
|
58
|
+
});
|
|
59
|
+
expect(result.kind).toBe("signed");
|
|
60
|
+
if (result.kind === "signed") {
|
|
61
|
+
expect(result.raw).toBe(FAKE_RAW);
|
|
62
|
+
expect(result.address).toBe(FAKE_ADDRESS);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("keystore method routes to wallet client and returns signed result", async () => {
|
|
67
|
+
const result = await signTransaction({
|
|
68
|
+
walletMethod: { type: "keystore", path: "/fake/keystore.json" },
|
|
69
|
+
tx: baseTx,
|
|
70
|
+
chain: sepolia,
|
|
71
|
+
});
|
|
72
|
+
expect(result.kind).toBe("signed");
|
|
73
|
+
if (result.kind === "signed") {
|
|
74
|
+
expect(result.raw).toBe(FAKE_RAW);
|
|
75
|
+
expect(result.address).toBe(FAKE_ADDRESS);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Chain } from "viem";
|
|
2
|
+
import { signWithBrowser } from "./browser";
|
|
3
|
+
import { createWalletFromMethod, type WalletMethod } from "./index";
|
|
4
|
+
|
|
5
|
+
export interface TxRequest {
|
|
6
|
+
to: `0x${string}`;
|
|
7
|
+
data: `0x${string}`;
|
|
8
|
+
gas?: bigint;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SignOptions {
|
|
12
|
+
browser?: { chainId: number; chainName: string; uri?: string; mode?: "register" | "update" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SignTransactionResult =
|
|
16
|
+
| { kind: "signed"; raw: `0x${string}`; address: `0x${string}` }
|
|
17
|
+
| { kind: "sent"; txHash: `0x${string}` };
|
|
18
|
+
|
|
19
|
+
export interface SignTransactionParams {
|
|
20
|
+
walletMethod: WalletMethod;
|
|
21
|
+
tx: TxRequest;
|
|
22
|
+
chain: Chain;
|
|
23
|
+
rpcUrl?: string;
|
|
24
|
+
options?: SignOptions;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function signTransaction({
|
|
28
|
+
walletMethod,
|
|
29
|
+
tx,
|
|
30
|
+
chain,
|
|
31
|
+
rpcUrl,
|
|
32
|
+
options,
|
|
33
|
+
}: SignTransactionParams): Promise<SignTransactionResult> {
|
|
34
|
+
switch (walletMethod.type) {
|
|
35
|
+
case "browser": {
|
|
36
|
+
if (!options?.browser) {
|
|
37
|
+
throw new Error("Browser wallet requires chainId and chainName parameters");
|
|
38
|
+
}
|
|
39
|
+
return signViaBrowser({
|
|
40
|
+
tx,
|
|
41
|
+
chainId: options.browser.chainId,
|
|
42
|
+
chainName: options.browser.chainName,
|
|
43
|
+
uri: options.browser.uri,
|
|
44
|
+
mode: options.browser.mode,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
default: {
|
|
48
|
+
const { raw, address } = await signWithWalletClient({ method: walletMethod, tx, chain, rpcUrl });
|
|
49
|
+
return { kind: "signed", raw, address };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function signViaBrowser({
|
|
55
|
+
tx,
|
|
56
|
+
chainId,
|
|
57
|
+
chainName,
|
|
58
|
+
uri,
|
|
59
|
+
mode,
|
|
60
|
+
}: {
|
|
61
|
+
tx: TxRequest;
|
|
62
|
+
chainId: number;
|
|
63
|
+
chainName: string;
|
|
64
|
+
uri?: string;
|
|
65
|
+
mode?: "register" | "update";
|
|
66
|
+
}): Promise<SignTransactionResult> {
|
|
67
|
+
const { txHash } = await signWithBrowser({
|
|
68
|
+
registryAddress: tx.to,
|
|
69
|
+
calldata: tx.data,
|
|
70
|
+
chainId,
|
|
71
|
+
chainName,
|
|
72
|
+
uri,
|
|
73
|
+
gas: tx.gas,
|
|
74
|
+
mode,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (typeof txHash !== "string" || !/^0x[0-9a-f]{64}$/i.test(txHash)) {
|
|
78
|
+
throw new Error(`Invalid transaction hash received from browser wallet: ${txHash}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { kind: "sent", txHash: txHash as `0x${string}` };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function signWithWalletClient({
|
|
85
|
+
method,
|
|
86
|
+
tx,
|
|
87
|
+
chain,
|
|
88
|
+
rpcUrl,
|
|
89
|
+
}: {
|
|
90
|
+
method: WalletMethod;
|
|
91
|
+
tx: TxRequest;
|
|
92
|
+
chain: Chain;
|
|
93
|
+
rpcUrl?: string;
|
|
94
|
+
}): Promise<{ raw: `0x${string}`; address: `0x${string}` }> {
|
|
95
|
+
const walletClient = await createWalletFromMethod(method, chain, rpcUrl);
|
|
96
|
+
|
|
97
|
+
const account = walletClient.account;
|
|
98
|
+
if (!account) {
|
|
99
|
+
throw new Error("Wallet client does not have an account configured");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const request = await walletClient.prepareTransactionRequest({
|
|
103
|
+
to: tx.to,
|
|
104
|
+
data: tx.data,
|
|
105
|
+
chain,
|
|
106
|
+
account,
|
|
107
|
+
...(tx.gas ? { gas: tx.gas } : {}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const raw = await walletClient.signTransaction(request);
|
|
111
|
+
return { raw, address: account.address };
|
|
112
|
+
}
|