@crossmint/client-sdk-smart-wallet 0.1.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/LICENSE +201 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +289 -0
- package/dist/index.d.ts +289 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/src/ABI/ERC1155.json +325 -0
- package/src/ABI/ERC20.json +222 -0
- package/src/ABI/ERC721.json +320 -0
- package/src/SmartWalletSDK.test.ts +32 -0
- package/src/SmartWalletSDK.ts +75 -0
- package/src/api/APIErrorService.ts +72 -0
- package/src/api/BaseCrossmintService.ts +82 -0
- package/src/api/CrossmintWalletService.test.ts +42 -0
- package/src/api/CrossmintWalletService.ts +50 -0
- package/src/blockchain/BlockchainNetworks.ts +121 -0
- package/src/blockchain/token/index.ts +1 -0
- package/src/blockchain/transfer.ts +54 -0
- package/src/blockchain/wallets/EVMSmartWallet.ts +109 -0
- package/src/blockchain/wallets/clientDecorator.ts +127 -0
- package/src/blockchain/wallets/eoa.ts +49 -0
- package/src/blockchain/wallets/index.ts +1 -0
- package/src/blockchain/wallets/passkey.ts +117 -0
- package/src/blockchain/wallets/paymaster.ts +49 -0
- package/src/blockchain/wallets/service.ts +193 -0
- package/src/error/index.ts +148 -0
- package/src/error/processor.ts +36 -0
- package/src/index.ts +34 -0
- package/src/services/logging/BrowserLoggerInterface.ts +5 -0
- package/src/services/logging/ConsoleProvider.ts +24 -0
- package/src/services/logging/DatadogProvider.ts +39 -0
- package/src/services/logging/index.ts +16 -0
- package/src/types/API.ts +40 -0
- package/src/types/Config.ts +35 -0
- package/src/types/Tokens.ts +27 -0
- package/src/types/internal.ts +50 -0
- package/src/utils/blockchain.ts +15 -0
- package/src/utils/constants.ts +31 -0
- package/src/utils/environment.ts +3 -0
- package/src/utils/helpers.ts +15 -0
- package/src/utils/log.test.ts +76 -0
- package/src/utils/log.ts +157 -0
- package/src/utils/signer.ts +36 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUNDLER_RPC,
|
|
3
|
+
PM_BASE_RPC,
|
|
4
|
+
PM_BASE_SEPOLIA_RPC,
|
|
5
|
+
ZD_AMOY_PROJECT_ID,
|
|
6
|
+
ZD_ARBITRUM_NOVA_PROJECT_ID,
|
|
7
|
+
ZD_ARBITRUM_PROJECT_ID,
|
|
8
|
+
ZD_ARBITRUM_SEPOLIA_PROJECT_ID,
|
|
9
|
+
ZD_ASTAR_PROJECT_ID,
|
|
10
|
+
ZD_BASE_PROJECT_ID,
|
|
11
|
+
ZD_BASE_SEPOLIA_PROJECT_ID,
|
|
12
|
+
ZD_BSC_PROJECT_ID,
|
|
13
|
+
ZD_ETHEREUM_PROJECT_ID,
|
|
14
|
+
ZD_GOERLI_PROJECT_ID,
|
|
15
|
+
ZD_OPTIMISM_PROJECT_ID,
|
|
16
|
+
ZD_OPTIMISM_SEPOLIA_PROJECT_ID,
|
|
17
|
+
ZD_POLYGON_PROJECT_ID,
|
|
18
|
+
ZD_SEPOLIA_PROJECT_ID,
|
|
19
|
+
ZD_ZKATANA_PROJECT_ID,
|
|
20
|
+
ZD_ZKYOTO_PROJECT_ID,
|
|
21
|
+
} from "@/utils/constants";
|
|
22
|
+
import {
|
|
23
|
+
arbitrum,
|
|
24
|
+
arbitrumNova,
|
|
25
|
+
arbitrumSepolia,
|
|
26
|
+
astarZkEVM,
|
|
27
|
+
astarZkyoto,
|
|
28
|
+
base,
|
|
29
|
+
baseSepolia,
|
|
30
|
+
bsc,
|
|
31
|
+
goerli,
|
|
32
|
+
mainnet,
|
|
33
|
+
optimism,
|
|
34
|
+
optimismSepolia,
|
|
35
|
+
polygon,
|
|
36
|
+
polygonAmoy,
|
|
37
|
+
sepolia,
|
|
38
|
+
} from "viem/chains";
|
|
39
|
+
|
|
40
|
+
import { EVMBlockchainIncludingTestnet } from "@crossmint/common-sdk-base";
|
|
41
|
+
|
|
42
|
+
export const getZeroDevProjectIdByBlockchain = (chain: EVMBlockchainIncludingTestnet) => {
|
|
43
|
+
const zeroDevProjectId = new Map<EVMBlockchainIncludingTestnet, string | null>([
|
|
44
|
+
["ethereum", ZD_ETHEREUM_PROJECT_ID],
|
|
45
|
+
["polygon", ZD_POLYGON_PROJECT_ID],
|
|
46
|
+
["bsc", ZD_BSC_PROJECT_ID],
|
|
47
|
+
["optimism", ZD_OPTIMISM_PROJECT_ID],
|
|
48
|
+
["arbitrum", ZD_ARBITRUM_PROJECT_ID],
|
|
49
|
+
["ethereum-goerli", ZD_GOERLI_PROJECT_ID],
|
|
50
|
+
["ethereum-sepolia", ZD_SEPOLIA_PROJECT_ID],
|
|
51
|
+
["polygon-amoy", ZD_AMOY_PROJECT_ID],
|
|
52
|
+
["zkatana", ZD_ZKATANA_PROJECT_ID],
|
|
53
|
+
["zkyoto", ZD_ZKYOTO_PROJECT_ID],
|
|
54
|
+
["arbitrum-sepolia", ZD_ARBITRUM_SEPOLIA_PROJECT_ID],
|
|
55
|
+
["base-goerli", null],
|
|
56
|
+
["base-sepolia", ZD_BASE_SEPOLIA_PROJECT_ID],
|
|
57
|
+
["bsc-testnet", null],
|
|
58
|
+
["optimism-goerli", null],
|
|
59
|
+
["optimism-sepolia", ZD_OPTIMISM_SEPOLIA_PROJECT_ID],
|
|
60
|
+
["zora-goerli", null],
|
|
61
|
+
["zora-sepolia", null],
|
|
62
|
+
["base", ZD_BASE_PROJECT_ID],
|
|
63
|
+
["zora", null],
|
|
64
|
+
["arbitrumnova", ZD_ARBITRUM_NOVA_PROJECT_ID],
|
|
65
|
+
["astar-zkevm", ZD_ASTAR_PROJECT_ID],
|
|
66
|
+
["apex", null],
|
|
67
|
+
]).get(chain)!;
|
|
68
|
+
|
|
69
|
+
if (zeroDevProjectId == null) {
|
|
70
|
+
throw new Error(`ZeroDev project id not found for chain ${chain}`);
|
|
71
|
+
}
|
|
72
|
+
return zeroDevProjectId;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const getViemNetwork = (cmChain: EVMBlockchainIncludingTestnet) => {
|
|
76
|
+
switch (cmChain) {
|
|
77
|
+
case "ethereum":
|
|
78
|
+
return mainnet;
|
|
79
|
+
case "ethereum-goerli":
|
|
80
|
+
return goerli;
|
|
81
|
+
case "ethereum-sepolia":
|
|
82
|
+
return sepolia;
|
|
83
|
+
case "polygon":
|
|
84
|
+
return polygon;
|
|
85
|
+
case "polygon-amoy":
|
|
86
|
+
return polygonAmoy;
|
|
87
|
+
case "optimism":
|
|
88
|
+
return optimism;
|
|
89
|
+
case "optimism-sepolia":
|
|
90
|
+
return optimismSepolia;
|
|
91
|
+
case "arbitrum":
|
|
92
|
+
return arbitrum;
|
|
93
|
+
case "arbitrumnova":
|
|
94
|
+
return arbitrumNova;
|
|
95
|
+
case "arbitrum-sepolia":
|
|
96
|
+
return arbitrumSepolia;
|
|
97
|
+
case "base":
|
|
98
|
+
return base;
|
|
99
|
+
case "base-sepolia":
|
|
100
|
+
return baseSepolia;
|
|
101
|
+
case "zkyoto":
|
|
102
|
+
return astarZkyoto;
|
|
103
|
+
case "astar-zkevm":
|
|
104
|
+
return astarZkEVM;
|
|
105
|
+
case "bsc":
|
|
106
|
+
return bsc;
|
|
107
|
+
default:
|
|
108
|
+
throw new Error(`Unsupported network: ${cmChain}`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const getBundlerRPC = (chain: EVMBlockchainIncludingTestnet) => {
|
|
113
|
+
switch (chain) {
|
|
114
|
+
case EVMBlockchainIncludingTestnet.BASE_SEPOLIA:
|
|
115
|
+
return PM_BASE_SEPOLIA_RPC;
|
|
116
|
+
case EVMBlockchainIncludingTestnet.BASE:
|
|
117
|
+
return PM_BASE_RPC;
|
|
118
|
+
default:
|
|
119
|
+
return BUNDLER_RPC + getZeroDevProjectIdByBlockchain(chain) + "?bundlerProvider=STACKUP";
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Tokens";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Abi, Account, Address, erc20Abi, erc721Abi } from "viem";
|
|
2
|
+
|
|
3
|
+
import erc1155Abi from "../ABI/ERC1155.json";
|
|
4
|
+
import type { ERC20TransferType, SFTTransferType, TransferType } from "../types/Tokens";
|
|
5
|
+
|
|
6
|
+
type TransferInputParams = {
|
|
7
|
+
from: Account;
|
|
8
|
+
contract: Address;
|
|
9
|
+
to: Address;
|
|
10
|
+
config: TransferType;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type TransferSimulationParams = {
|
|
14
|
+
account: Account;
|
|
15
|
+
address: Address;
|
|
16
|
+
abi: Abi;
|
|
17
|
+
functionName: string;
|
|
18
|
+
args: any[];
|
|
19
|
+
tokenId?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function transferParams({ contract, config, from, to }: TransferInputParams): TransferSimulationParams {
|
|
23
|
+
switch (config.token.type) {
|
|
24
|
+
case "ft": {
|
|
25
|
+
return {
|
|
26
|
+
account: from,
|
|
27
|
+
address: contract,
|
|
28
|
+
abi: erc20Abi,
|
|
29
|
+
functionName: "transfer",
|
|
30
|
+
args: [to, (config as ERC20TransferType).amount],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
case "sft": {
|
|
34
|
+
return {
|
|
35
|
+
account: from,
|
|
36
|
+
address: contract,
|
|
37
|
+
abi: erc1155Abi as Abi,
|
|
38
|
+
functionName: "safeTransferFrom",
|
|
39
|
+
args: [from.address, to, config.token.tokenId, (config as SFTTransferType).quantity, "0x00"],
|
|
40
|
+
tokenId: config.token.tokenId,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
case "nft": {
|
|
44
|
+
return {
|
|
45
|
+
account: from,
|
|
46
|
+
address: contract,
|
|
47
|
+
abi: erc721Abi,
|
|
48
|
+
functionName: "safeTransferFrom",
|
|
49
|
+
args: [from.address, to, config.token.tokenId],
|
|
50
|
+
tokenId: config.token.tokenId,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { logError } from "@/services/logging";
|
|
2
|
+
import { type HttpTransport, type PublicClient, isAddress, publicActions } from "viem";
|
|
3
|
+
|
|
4
|
+
import { EVMBlockchainIncludingTestnet } from "@crossmint/common-sdk-base";
|
|
5
|
+
|
|
6
|
+
import type { CrossmintWalletService } from "../../api/CrossmintWalletService";
|
|
7
|
+
import { TransferError } from "../../error";
|
|
8
|
+
import type { TransferType } from "../../types/Tokens";
|
|
9
|
+
import { SmartWalletClient } from "../../types/internal";
|
|
10
|
+
import { SCW_SERVICE } from "../../utils/constants";
|
|
11
|
+
import { LoggerWrapper, errorToJSON } from "../../utils/log";
|
|
12
|
+
import { transferParams } from "../transfer";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Smart wallet interface for EVM chains enhanced with Crossmint capabilities.
|
|
16
|
+
* Core functionality is exposed via [viem](https://viem.sh/) clients within the `client` property of the class.
|
|
17
|
+
*/
|
|
18
|
+
export class EVMSmartWallet extends LoggerWrapper {
|
|
19
|
+
public readonly chain: EVMBlockchainIncludingTestnet;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* [viem](https://viem.sh/) clients that provide an interface for core wallet functionality.
|
|
23
|
+
*/
|
|
24
|
+
public readonly client: {
|
|
25
|
+
/**
|
|
26
|
+
* An interface to interact with the smart wallet, execute transactions, sign messages, etc.
|
|
27
|
+
*/
|
|
28
|
+
wallet: SmartWalletClient;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An interface to read onchain data, fetch transactions, retrieve account balances, etc. Corresponds to public [JSON-RPC API](https://ethereum.org/en/developers/docs/apis/json-rpc/) methods.
|
|
32
|
+
*/
|
|
33
|
+
public: PublicClient;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly crossmintService: CrossmintWalletService,
|
|
38
|
+
private readonly accountClient: SmartWalletClient,
|
|
39
|
+
publicClient: PublicClient<HttpTransport>,
|
|
40
|
+
chain: EVMBlockchainIncludingTestnet
|
|
41
|
+
) {
|
|
42
|
+
super("EVMSmartWallet", { chain, address: accountClient.account.address });
|
|
43
|
+
this.chain = chain;
|
|
44
|
+
this.client = {
|
|
45
|
+
wallet: accountClient,
|
|
46
|
+
public: publicClient,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The address of the smart wallet.
|
|
52
|
+
*/
|
|
53
|
+
public get address() {
|
|
54
|
+
return this.accountClient.account.address;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @returns The transaction hash.
|
|
59
|
+
*/
|
|
60
|
+
public async transferToken(toAddress: string, config: TransferType): Promise<string> {
|
|
61
|
+
return this.logPerformance("TRANSFER", async () => {
|
|
62
|
+
if (this.chain !== config.token.chain) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Chain mismatch: Expected ${config.token.chain}, but got ${this.chain}. Ensure you are interacting with the correct blockchain.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!isAddress(toAddress)) {
|
|
69
|
+
throw new Error(`Invalid recipient address: '${toAddress}' is not a valid EVM address.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isAddress(config.token.contractAddress)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Invalid contract address: '${config.token.contractAddress}' is not a valid EVM address.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const tx = transferParams({
|
|
79
|
+
contract: config.token.contractAddress,
|
|
80
|
+
to: toAddress,
|
|
81
|
+
from: this.accountClient.account,
|
|
82
|
+
config,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const client = this.accountClient.extend(publicActions);
|
|
87
|
+
const { request } = await client.simulateContract(tx);
|
|
88
|
+
return await client.writeContract(request);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logError("[TRANSFER] - ERROR_TRANSFERRING_TOKEN", {
|
|
91
|
+
service: SCW_SERVICE,
|
|
92
|
+
error: errorToJSON(error),
|
|
93
|
+
tokenId: tx.tokenId,
|
|
94
|
+
contractAddress: config.token.contractAddress,
|
|
95
|
+
chain: config.token.chain,
|
|
96
|
+
});
|
|
97
|
+
const tokenIdString = tx.tokenId == null ? "" : `:${tx.tokenId}}`;
|
|
98
|
+
throw new TransferError(`Error transferring token ${config.token.contractAddress}${tokenIdString}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @returns A list of NFTs owned by the wallet.
|
|
105
|
+
*/
|
|
106
|
+
public async nfts() {
|
|
107
|
+
return this.crossmintService.fetchNFTs(this.address, this.chain);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { SmartWalletSDKError } from "@/error";
|
|
2
|
+
import { ErrorProcessor } from "@/error/processor";
|
|
3
|
+
import { logInfo } from "@/services/logging";
|
|
4
|
+
import { usesGelatoBundler } from "@/utils/blockchain";
|
|
5
|
+
import { logPerformance } from "@/utils/log";
|
|
6
|
+
import type { SmartAccountClient } from "permissionless";
|
|
7
|
+
import type { EntryPoint } from "permissionless/types/entrypoint";
|
|
8
|
+
import { stringify } from "viem";
|
|
9
|
+
|
|
10
|
+
import { EVMBlockchainIncludingTestnet } from "@crossmint/common-sdk-base";
|
|
11
|
+
|
|
12
|
+
const transactionMethods = [
|
|
13
|
+
"sendTransaction",
|
|
14
|
+
"writeContract",
|
|
15
|
+
"sendUserOperation",
|
|
16
|
+
] as const satisfies readonly (keyof SmartAccountClient<EntryPoint>)[];
|
|
17
|
+
|
|
18
|
+
const signingMethods = [
|
|
19
|
+
"signMessage",
|
|
20
|
+
"signTypedData",
|
|
21
|
+
] as const satisfies readonly (keyof SmartAccountClient<EntryPoint>)[];
|
|
22
|
+
|
|
23
|
+
type TxnMethod = (typeof transactionMethods)[number];
|
|
24
|
+
type SignMethod = (typeof signingMethods)[number];
|
|
25
|
+
|
|
26
|
+
function isTxnMethod(method: string): method is TxnMethod {
|
|
27
|
+
return transactionMethods.includes(method as any);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isSignMethod(method: string): method is SignMethod {
|
|
31
|
+
return signingMethods.includes(method as any);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A decorator class for SmartAccountClient instances. It enhances the client with:
|
|
36
|
+
* - Error handling & logging.
|
|
37
|
+
* - Performance metrics.
|
|
38
|
+
* - Automatic formatting of transactions for Gelato bundler compatibility.
|
|
39
|
+
* */
|
|
40
|
+
export class ClientDecorator {
|
|
41
|
+
constructor(private readonly errorProcessor: ErrorProcessor) {}
|
|
42
|
+
|
|
43
|
+
public decorate<Client extends SmartAccountClient<EntryPoint>>({
|
|
44
|
+
crossmintChain,
|
|
45
|
+
smartAccountClient,
|
|
46
|
+
}: {
|
|
47
|
+
crossmintChain: EVMBlockchainIncludingTestnet;
|
|
48
|
+
smartAccountClient: Client;
|
|
49
|
+
}): Client {
|
|
50
|
+
return new Proxy(smartAccountClient, {
|
|
51
|
+
get: (target, prop, receiver) => {
|
|
52
|
+
const originalMethod = Reflect.get(target, prop, receiver);
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
typeof originalMethod !== "function" ||
|
|
56
|
+
typeof prop !== "string" ||
|
|
57
|
+
!(isSignMethod(prop) || isTxnMethod(prop))
|
|
58
|
+
) {
|
|
59
|
+
return originalMethod;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (...args: any[]) =>
|
|
63
|
+
logPerformance(`CrossmintSmartWallet.${prop}`, () =>
|
|
64
|
+
this.execute(target, prop, originalMethod, args, crossmintChain)
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
}) as Client;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async execute<M extends TxnMethod | SignMethod>(
|
|
71
|
+
target: SmartAccountClient<EntryPoint>,
|
|
72
|
+
prop: M,
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
74
|
+
originalMethod: Function,
|
|
75
|
+
args: any[],
|
|
76
|
+
crossmintChain: EVMBlockchainIncludingTestnet
|
|
77
|
+
) {
|
|
78
|
+
try {
|
|
79
|
+
logInfo(`[CrossmintSmartWallet.${prop}] - params: ${stringify(args)}`);
|
|
80
|
+
const processed = isTxnMethod(prop) ? this.processTxnArgs(prop, crossmintChain, args) : args;
|
|
81
|
+
return await originalMethod.call(target, ...processed);
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
const description = isTxnMethod(prop) ? "signing" : "sending transaction";
|
|
84
|
+
throw this.errorProcessor.map(
|
|
85
|
+
error,
|
|
86
|
+
new SmartWalletSDKError(`Error ${description}: ${error.message}`, stringify(error))
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private processTxnArgs(prop: TxnMethod, crossmintChain: EVMBlockchainIncludingTestnet, args: any[]): any[] {
|
|
92
|
+
if (prop === "sendUserOperation") {
|
|
93
|
+
const [{ userOperation, middleware, account }] = args as Parameters<
|
|
94
|
+
SmartAccountClient<EntryPoint>["sendUserOperation"]
|
|
95
|
+
>;
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
middleware,
|
|
99
|
+
account,
|
|
100
|
+
userOperation: this.addGelatoBundlerProperties(crossmintChain, userOperation),
|
|
101
|
+
},
|
|
102
|
+
...args.slice(1),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const [txn] = args as
|
|
107
|
+
| Parameters<SmartAccountClient<EntryPoint>["sendTransaction"]>
|
|
108
|
+
| Parameters<SmartAccountClient<EntryPoint>["writeContract"]>;
|
|
109
|
+
|
|
110
|
+
return [this.addGelatoBundlerProperties(crossmintChain, txn), ...args.slice(1)];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* Chain that ZD uses Gelato as for bundler require special parameters:
|
|
115
|
+
* https://docs.zerodev.app/sdk/faqs/use-with-gelato#transaction-configuration
|
|
116
|
+
*/
|
|
117
|
+
private addGelatoBundlerProperties(
|
|
118
|
+
crossmintChain: EVMBlockchainIncludingTestnet,
|
|
119
|
+
txnParams: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint }
|
|
120
|
+
) {
|
|
121
|
+
if (usesGelatoBundler(crossmintChain)) {
|
|
122
|
+
return { ...txnParams, maxFeePerGas: "0x0" as any, maxPriorityFeePerGas: "0x0" as any };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return txnParams;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EOASignerData } from "@/types/API";
|
|
2
|
+
import type { EOASigner, WalletParams } from "@/types/Config";
|
|
3
|
+
import { AccountAndSigner, WalletCreationParams } from "@/types/internal";
|
|
4
|
+
import { equalsIgnoreCase } from "@/utils/helpers";
|
|
5
|
+
import { createOwnerSigner } from "@/utils/signer";
|
|
6
|
+
import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator";
|
|
7
|
+
import { createKernelAccount } from "@zerodev/sdk";
|
|
8
|
+
|
|
9
|
+
import { AdminMismatchError } from "../../error";
|
|
10
|
+
|
|
11
|
+
export interface EOAWalletParams extends WalletCreationParams {
|
|
12
|
+
walletParams: WalletParams & { signer: EOASigner };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class EOAAccountService {
|
|
16
|
+
public async get(
|
|
17
|
+
{ chain, publicClient, entryPoint, walletParams, kernelVersion, user }: EOAWalletParams,
|
|
18
|
+
existingSignerConfig?: EOASignerData
|
|
19
|
+
): Promise<AccountAndSigner> {
|
|
20
|
+
const eoa = await createOwnerSigner({
|
|
21
|
+
chain,
|
|
22
|
+
walletParams,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (existingSignerConfig != null && !equalsIgnoreCase(eoa.address, existingSignerConfig.eoaAddress)) {
|
|
26
|
+
throw new AdminMismatchError(
|
|
27
|
+
`User '${user.id}' has an existing wallet with an eoa signer '${existingSignerConfig.eoaAddress}', this does not match input eoa signer '${eoa.address}'.`,
|
|
28
|
+
existingSignerConfig,
|
|
29
|
+
{ type: "eoa", eoaAddress: existingSignerConfig.eoaAddress }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ecdsaValidator = await signerToEcdsaValidator(publicClient, {
|
|
34
|
+
signer: eoa,
|
|
35
|
+
entryPoint: entryPoint.address,
|
|
36
|
+
kernelVersion,
|
|
37
|
+
});
|
|
38
|
+
const account = await createKernelAccount(publicClient, {
|
|
39
|
+
plugins: {
|
|
40
|
+
sudo: ecdsaValidator,
|
|
41
|
+
},
|
|
42
|
+
index: BigInt(0),
|
|
43
|
+
entryPoint: entryPoint.address,
|
|
44
|
+
kernelVersion,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { account, signerData: { eoaAddress: eoa.address, type: "eoa" } };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./EVMSmartWallet";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { CrossmintWalletService } from "@/api/CrossmintWalletService";
|
|
2
|
+
import { type PasskeySignerData, displayPasskey } from "@/types/API";
|
|
3
|
+
import type { PasskeySigner, UserParams, WalletParams } from "@/types/Config";
|
|
4
|
+
import type { AccountAndSigner, PasskeyValidatorSerializedData, WalletCreationParams } from "@/types/internal";
|
|
5
|
+
import { PasskeyValidatorContractVersion, WebAuthnMode, toPasskeyValidator } from "@zerodev/passkey-validator";
|
|
6
|
+
import { type KernelValidator, createKernelAccount } from "@zerodev/sdk";
|
|
7
|
+
import { WebAuthnKey, toWebAuthnKey } from "@zerodev/webauthn-key";
|
|
8
|
+
import type { EntryPoint } from "permissionless/types/entrypoint";
|
|
9
|
+
|
|
10
|
+
import { PasskeyMismatchError } from "../../error";
|
|
11
|
+
|
|
12
|
+
export interface PasskeyWalletParams extends WalletCreationParams {
|
|
13
|
+
walletParams: WalletParams & { signer: PasskeySigner };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isPasskeyParams(params: WalletCreationParams): params is PasskeyWalletParams {
|
|
17
|
+
return (params.walletParams.signer as PasskeySigner).type === "PASSKEY";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type PasskeyValidator = KernelValidator<EntryPoint, "WebAuthnValidator"> & {
|
|
21
|
+
getSerializedData: () => string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class PasskeyAccountService {
|
|
25
|
+
constructor(private readonly crossmintService: CrossmintWalletService) {}
|
|
26
|
+
|
|
27
|
+
public async get(
|
|
28
|
+
{ user, publicClient, walletParams, entryPoint, kernelVersion }: PasskeyWalletParams,
|
|
29
|
+
existingSignerConfig?: PasskeySignerData
|
|
30
|
+
): Promise<AccountAndSigner> {
|
|
31
|
+
const inputPasskeyName = walletParams.signer.passkeyName ?? user.id;
|
|
32
|
+
if (existingSignerConfig != null && existingSignerConfig.passkeyName !== inputPasskeyName) {
|
|
33
|
+
throw new PasskeyMismatchError(
|
|
34
|
+
`User '${user.id}' has an existing wallet created with a passkey named '${existingSignerConfig.passkeyName}', this does match input passkey name '${inputPasskeyName}'.`,
|
|
35
|
+
displayPasskey(existingSignerConfig)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const passkey = await this.getPasskey(user, inputPasskeyName, existingSignerConfig);
|
|
40
|
+
|
|
41
|
+
const latestValidatorVersion = PasskeyValidatorContractVersion.V0_0_2;
|
|
42
|
+
const validatorContractVersion =
|
|
43
|
+
existingSignerConfig == null ? latestValidatorVersion : existingSignerConfig.validatorContractVersion;
|
|
44
|
+
const validator = await toPasskeyValidator(publicClient, {
|
|
45
|
+
webAuthnKey: passkey,
|
|
46
|
+
entryPoint: entryPoint.address,
|
|
47
|
+
validatorContractVersion,
|
|
48
|
+
kernelVersion,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const kernelAccount = await createKernelAccount(publicClient, {
|
|
52
|
+
plugins: { sudo: validator },
|
|
53
|
+
entryPoint: entryPoint.address,
|
|
54
|
+
kernelVersion,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
signerData: this.getSignerData(validator, validatorContractVersion, inputPasskeyName),
|
|
59
|
+
account: kernelAccount,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async getPasskey(
|
|
64
|
+
user: UserParams,
|
|
65
|
+
passkeyName: string,
|
|
66
|
+
existing?: PasskeySignerData
|
|
67
|
+
): Promise<WebAuthnKey> {
|
|
68
|
+
if (existing != null) {
|
|
69
|
+
return {
|
|
70
|
+
pubX: BigInt(existing.pubKeyX),
|
|
71
|
+
pubY: BigInt(existing.pubKeyY),
|
|
72
|
+
authenticatorId: existing.authenticatorId,
|
|
73
|
+
authenticatorIdHash: existing.authenticatorIdHash,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return toWebAuthnKey({
|
|
78
|
+
passkeyName,
|
|
79
|
+
passkeyServerUrl: this.crossmintService.getPasskeyServerUrl(),
|
|
80
|
+
mode: WebAuthnMode.Register,
|
|
81
|
+
passkeyServerHeaders: this.createPasskeysServerHeaders(user),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private getSignerData(
|
|
86
|
+
validator: PasskeyValidator,
|
|
87
|
+
validatorContractVersion: PasskeyValidatorContractVersion,
|
|
88
|
+
passkeyName: string
|
|
89
|
+
): PasskeySignerData {
|
|
90
|
+
return {
|
|
91
|
+
...deserializePasskeyValidatorData(validator.getSerializedData()),
|
|
92
|
+
passkeyName,
|
|
93
|
+
validatorContractVersion,
|
|
94
|
+
domain: window.location.hostname,
|
|
95
|
+
type: "passkeys",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private createPasskeysServerHeaders(user: UserParams) {
|
|
100
|
+
return {
|
|
101
|
+
"x-api-key": this.crossmintService.crossmintAPIHeaders["x-api-key"],
|
|
102
|
+
Authorization: `Bearer ${user.jwt}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const deserializePasskeyValidatorData = (params: string) => {
|
|
108
|
+
const uint8Array = base64ToBytes(params);
|
|
109
|
+
const jsonString = new TextDecoder().decode(uint8Array);
|
|
110
|
+
|
|
111
|
+
return JSON.parse(jsonString) as PasskeyValidatorSerializedData;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function base64ToBytes(base64: string) {
|
|
115
|
+
const binString = atob(base64);
|
|
116
|
+
return Uint8Array.from(binString, (m) => m.codePointAt(0) as number);
|
|
117
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PAYMASTER_RPC, PM_BASE_RPC, PM_BASE_SEPOLIA_RPC } from "@/utils/constants";
|
|
2
|
+
import { createZeroDevPaymasterClient } from "@zerodev/sdk";
|
|
3
|
+
import { Middleware } from "permissionless/actions/smartAccount";
|
|
4
|
+
import { EntryPoint } from "permissionless/types/entrypoint";
|
|
5
|
+
import { http } from "viem";
|
|
6
|
+
|
|
7
|
+
import { EVMBlockchainIncludingTestnet } from "@crossmint/common-sdk-base";
|
|
8
|
+
|
|
9
|
+
import { usesGelatoBundler } from "../../utils/blockchain";
|
|
10
|
+
import { getViemNetwork, getZeroDevProjectIdByBlockchain } from "../BlockchainNetworks";
|
|
11
|
+
|
|
12
|
+
export function usePaymaster(chain: EVMBlockchainIncludingTestnet) {
|
|
13
|
+
return !usesGelatoBundler(chain);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const getPaymasterRPC = (chain: EVMBlockchainIncludingTestnet) => {
|
|
17
|
+
switch (chain) {
|
|
18
|
+
case EVMBlockchainIncludingTestnet.BASE_SEPOLIA:
|
|
19
|
+
return PM_BASE_SEPOLIA_RPC;
|
|
20
|
+
case EVMBlockchainIncludingTestnet.BASE:
|
|
21
|
+
return PM_BASE_RPC;
|
|
22
|
+
default:
|
|
23
|
+
return PAYMASTER_RPC + getZeroDevProjectIdByBlockchain(chain) + "?paymasterProvider=STACKUP";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function paymasterMiddleware({
|
|
28
|
+
entryPoint,
|
|
29
|
+
chain,
|
|
30
|
+
}: {
|
|
31
|
+
entryPoint: EntryPoint;
|
|
32
|
+
chain: EVMBlockchainIncludingTestnet;
|
|
33
|
+
}): Middleware<EntryPoint> {
|
|
34
|
+
return {
|
|
35
|
+
middleware: {
|
|
36
|
+
sponsorUserOperation: async ({ userOperation }) => {
|
|
37
|
+
const paymasterClient = createZeroDevPaymasterClient({
|
|
38
|
+
chain: getViemNetwork(chain),
|
|
39
|
+
transport: http(getPaymasterRPC(chain)),
|
|
40
|
+
entryPoint,
|
|
41
|
+
});
|
|
42
|
+
return paymasterClient.sponsorUserOperation({
|
|
43
|
+
userOperation,
|
|
44
|
+
entryPoint,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|