diff --git a/README.md b/README.md index 2619bdf0..f18da204 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ import { erc20Abi } from "viem"; const ssoConnector = zksyncSsoConnector({ // Optional session configuration, if omitted user will have to sign every transaction via Auth Server session: { + expiry: "1 day", + // Allow up to 0.1 ETH to be spend in gas fees feeLimit: parseEther("0.1"), @@ -61,7 +63,7 @@ const ssoConnector = zksyncSsoConnector({ abi: erc20Abi, functionName: "transfer", constraints: [ - // Only allow transfers to this address, any address if omitted + // Only allow transfers to this address. Or any address if omitted { index: 0, // First argument of erc20 transfer function, recipient address value: "0x6cC8cf7f6b488C58AA909B77E6e65c631c204784", @@ -73,7 +75,7 @@ const ssoConnector = zksyncSsoConnector({ index: 1, limit: { limit: parseUnits("0.2", TOKEN.decimals), - period: BigInt(60 * 60), // 1 hour in seconds + period: "1 hour", }, }, ], diff --git a/docs/sdk/client-auth-server/README.md b/docs/sdk/client-auth-server/README.md index 1dcbf2fe..d9503d3c 100644 --- a/docs/sdk/client-auth-server/README.md +++ b/docs/sdk/client-auth-server/README.md @@ -7,21 +7,53 @@ your application. It's built on top of [client SDK](../client/README.md) and ## Basic usage ```ts -import { zksync } from "viem/chains"; +import { zksyncSsoConnector, callPolicy } from "zksync-sso/connector"; +import { zksyncSepoliaTestnet } from "viem/chains"; import { createConfig, connect } from "@wagmi/core"; -import { zksyncSsoConnector } from "zksync-sso/connector"; +import { erc20Abi } from "viem"; const ssoConnector = zksyncSsoConnector({ // Optional session configuration + // if omitted user will have to sign every transaction via Auth Server session: { + expiry: "1 day", + + // Allow up to 0.1 ETH to be spend in gas fees feeLimit: parseEther("0.1"), - // Allow transfers to a specific address with a limit of 0.1 ETH + transfers: [ + // Allow ETH transfers of up to 0.1 ETH to specific address { to: "0x188bd99cd7D4d78d4E605Aeea12C17B32CC3135A", valueLimit: parseEther("0.1"), }, ], + + // Allow calling specific smart contracts (e.g. ERC20 transfer): + contractCalls: [ + callPolicy({ + address: "0xa1cf087DB965Ab02Fb3CFaCe1f5c63935815f044", + abi: erc20Abi, + functionName: "transfer", + constraints: [ + // Only allow transfers to this address. Or any address if omitted + { + index: 0, // First argument of erc20 transfer function, recipient address + value: "0x6cC8cf7f6b488C58AA909B77E6e65c631c204784", + }, + + // Allow transfering up to 0.2 tokens per hour + // until the session expires + { + index: 1, + limit: { + limit: parseUnits("0.2", TOKEN.decimals), + period: "1 hour", + }, + }, + ], + }), + ], }, }); @@ -33,7 +65,7 @@ const wagmiConfig = createConfig({ const connectWithSSO = () => { connect(wagmiConfig, { connector: ssoConnector, - chainId: zksync.id, // or another chain id that has SSO support + chainId: zksyncSepoliaTestnet.id, // or another chain id that has SSO support }); }; ``` diff --git a/examples/nft-quest/stores/connector.ts b/examples/nft-quest/stores/connector.ts index 0c2b8649..2e18db76 100644 --- a/examples/nft-quest/stores/connector.ts +++ b/examples/nft-quest/stores/connector.ts @@ -3,7 +3,7 @@ import { zksyncInMemoryNode, zksyncLocalNode, zksyncSepoliaTestnet } from "@wagm import { type Address, type Hash, parseEther } from "viem"; import { callPolicy, zksyncSsoConnector } from "zksync-sso/connector"; -import { ZeekNftQuestAbi } from "@/abi/ZeekNftQuest"; +import { ZeekNftQuestAbi } from "@/abi/ZeekNFTQuest"; export const useConnectorStore = defineStore("connector", () => { const runtimeConfig = useRuntimeConfig(); diff --git a/packages/sdk/README.md b/packages/sdk/README.md index fcf9121f..9dcea58b 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -39,9 +39,10 @@ import { createConfig, connect } from "@wagmi/core"; import { erc20Abi } from "viem"; const ssoConnector = zksyncSsoConnector({ - // Optional session configuration, - // if omitted user will have to sign every transaction via Auth Server + // Optional session configuration, if omitted user will have to sign every transaction via Auth Server session: { + expiry: "1 day", + // Allow up to 0.1 ETH to be spend in gas fees feeLimit: parseEther("0.1"), @@ -60,7 +61,7 @@ const ssoConnector = zksyncSsoConnector({ abi: erc20Abi, functionName: "transfer", constraints: [ - // Only allow transfers to this address, any address if omitted + // Only allow transfers to this address. Or any address if omitted { index: 0, // First argument of erc20 transfer function, recipient address value: "0x6cC8cf7f6b488C58AA909B77E6e65c631c204784", @@ -71,26 +72,26 @@ const ssoConnector = zksyncSsoConnector({ { index: 1, limit: { - limit: parseUnits("0.2", TOKEN.decimals), - period: BigInt(60 * 60), // 1 hour in seconds + limit: parseUnits("0.2", TOKEN.decimals), + period: "1 hour", }, }, ], }), ], - }, + }, }); const wagmiConfig = createConfig({ - connectors: [ssoConnector], - ..., // your wagmi config https://wagmi.sh/core/api/createConfig + connectors: [ssoConnector], + ..., // your wagmi config https://wagmi.sh/core/api/createConfig }); const connectWithSSO = () => { - connect(wagmiConfig, { - connector: ssoConnector, - chainId: zksyncSepoliaTestnet.id, // or another chain id that has SSO support - }); + connect(wagmiConfig, { + connector: ssoConnector, + chainId: zksyncSepoliaTestnet.id, // or another chain id that has SSO support + }); }; ``` diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b8256b78..245a7e44 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@simplewebauthn/types": "^10.0.0", + "@types/ms": "^0.7.34", "@types/node": "^22.1.0", "eventemitter3": "^5.0.1", "viem": "2.21.14" @@ -144,6 +145,7 @@ "@peculiar/asn1-schema": "^2.3.13", "abitype": "^1.0.6", "bigint-conversion": "^2.4.3", - "buffer": "^6.0.3" + "buffer": "^6.0.3", + "ms": "^2.1.3" } } diff --git a/packages/sdk/src/client-auth-server/session/index.ts b/packages/sdk/src/client-auth-server/session/index.ts index d48ffeb7..7470d16f 100644 --- a/packages/sdk/src/client-auth-server/session/index.ts +++ b/packages/sdk/src/client-auth-server/session/index.ts @@ -2,11 +2,11 @@ import { type Abi, type AbiFunction, type AbiStateMutability, type Address, type import { ConstraintCondition, type Limit, LimitType, LimitUnlimited, LimitZero, type SessionConfig } from "../../utils/session.js"; import type { ContractWriteMutability, IndexedValues } from "./type-utils.js"; -import { encodedInputToAbiChunks, getParameterChunkIndex, isDynamicInputType, isFollowedByDynamicInputType } from "./utils.js"; +import { encodedInputToAbiChunks, getParameterChunkIndex, isDynamicInputType, isFollowedByDynamicInputType, msStringToSeconds } from "./utils.js"; export type PartialLimit = bigint | { limit: bigint; - period?: bigint; + period?: string | bigint; } | { limitType: "lifetime" | LimitType.Lifetime; limit: bigint; @@ -15,7 +15,7 @@ export type PartialLimit = bigint | { } | { limitType: "allowance" | LimitType.Allowance; limit: bigint; - period: bigint; + period: string | bigint; }; export type PartialCallPolicy = { @@ -61,7 +61,7 @@ export type PartialTransferPolicy = { }; export interface SessionPreferences { - expiresAt?: bigint | Date; + expiry?: string | bigint | Date; feeLimit?: PartialLimit; contractCalls?: PartialCallPolicy[]; transfers?: PartialTransferPolicy[]; @@ -95,7 +95,7 @@ export const formatLimitPreferences = (limit: PartialLimit): Limit => { return { limitType: LimitType.Allowance, limit: limit.limit, - period: limit.period, + period: typeof limit.period === "string" ? msStringToSeconds(limit.period) : limit.period, }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -103,23 +103,29 @@ export const formatLimitPreferences = (limit: PartialLimit): Limit => { } /* LimitType not selected */ - if (!limit.period) { + if (limit.period) { return { - limitType: LimitType.Lifetime, + limitType: LimitType.Allowance, limit: limit.limit, - period: 0n, + period: typeof limit.period === "string" ? msStringToSeconds(limit.period) : limit.period, }; } return { - limitType: LimitType.Allowance, + limitType: LimitType.Lifetime, limit: limit.limit, - period: limit.period, + period: 0n, }; }; -export const formatDatePreferences = (date: bigint | Date): bigint => { +export const formatDatePreferences = (date: string | bigint | Date): bigint => { + if (typeof date === "string") { + const now = new Date().getTime(); + const seconds = msStringToSeconds(date); + return BigInt(now) + seconds; + } if (date instanceof Date) { - return BigInt(Math.floor(date.getTime() / 1000)); + const seconds = Math.floor(date.getTime() / 1000); + return BigInt(seconds); } return date; }; @@ -132,7 +138,7 @@ export function formatSessionPreferences( }, ): Omit { return { - expiresAt: preferences.expiresAt ? formatDatePreferences(preferences.expiresAt) : defaults.expiresAt, + expiresAt: preferences.expiry ? formatDatePreferences(preferences.expiry) : defaults.expiresAt, feeLimit: preferences.feeLimit ? formatLimitPreferences(preferences.feeLimit) : defaults.feeLimit, callPolicies: preferences.contractCalls?.map((policy) => { const allowedStateMutability: ContractWriteMutability[] = ["nonpayable", "payable"]; @@ -150,10 +156,6 @@ export function formatSessionPreferences( constraints: policy.constraints?.map((constraint) => { const limit = constraint.limit ? formatLimitPreferences(constraint.limit) : LimitUnlimited; const condition = constraint.condition ? ConstraintCondition[constraint.condition] : ConstraintCondition.Unconstrained; - /* index: BigInt(constraint.index), - condition, - refValue: encodedInput ?? toHex("", { size: 32 }), - limit: constraint.limit ? formatLimitPreferences(constraint.limit) : LimitUnlimited, */ const input = abiFunction.inputs[constraint.index]; if (!input) { @@ -171,7 +173,6 @@ export function formatSessionPreferences( } const startingAbiChunkIndex = getParameterChunkIndex(abiFunction, constraint.index); - console.log("startingAbiChunkIndex", startingAbiChunkIndex); if (constraint.value === undefined || constraint.value === null) { return { index: BigInt(startingAbiChunkIndex), diff --git a/packages/sdk/src/client-auth-server/session/utils.ts b/packages/sdk/src/client-auth-server/session/utils.ts index fb366d5c..36fe2552 100644 --- a/packages/sdk/src/client-auth-server/session/utils.ts +++ b/packages/sdk/src/client-auth-server/session/utils.ts @@ -1,3 +1,4 @@ +import ms from "ms"; import { type AbiFunction, type AbiParameter, type Address, encodeAbiParameters, type Hash, toHex } from "viem"; const DYNAMIC_ABI_INPUT_TYPES = ["bytes", "string"]; @@ -102,14 +103,15 @@ export const getParameterChunkIndex = ( return chunkIndex; }; -/* SessionKeyModuleAbi.forEach((abi) => { - if (abi.type !== "function") return; - const dummyValues = getDummyValues(abi.inputs); - - console.log(abi.name, abi.inputs, dummyValues); +export const msStringToSeconds = (value: string): bigint => { + let millis: number; try { - console.log("Encoded", encodeAbiParameters(abi.inputs, dummyValues as any)); + millis = ms(value); } catch (error) { - console.error("Error", error); + throw new Error(`Invalid date format: ${value}: ${error}`); } -}); */ + if (millis < 0) throw new Error(`Date can't be in the past: ${value}`); + if (millis === 0) throw new Error(`Date can't be zero: ${value}`); + const seconds = Math.floor(millis / 1000); + return BigInt(seconds); +}; diff --git a/packages/sdk/tsconfig.base.json b/packages/sdk/tsconfig.base.json index 6f842535..549cb533 100644 --- a/packages/sdk/tsconfig.base.json +++ b/packages/sdk/tsconfig.base.json @@ -25,7 +25,7 @@ // Interop constraints "esModuleInterop": false, - "allowSyntheticDefaultImports": false, + "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "verbatimModuleSyntax": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b642aa9b..32bc354e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,7 +149,7 @@ importers: version: 3.5.13(typescript@5.6.2) vue-router: specifier: latest - version: 4.4.5(vue@3.5.13(typescript@5.6.2)) + version: 4.5.0(vue@3.5.13(typescript@5.6.2)) zksync-sso: specifier: workspace:* version: link:../../packages/sdk @@ -297,7 +297,7 @@ importers: version: 1.1.0(vue@3.5.13(typescript@5.6.2)) vue-router: specifier: latest - version: 4.4.5(vue@3.5.13(typescript@5.6.2)) + version: 4.5.0(vue@3.5.13(typescript@5.6.2)) zksync-ethers: specifier: ^6.15.0 version: 6.15.0(ethers@6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)) @@ -567,10 +567,16 @@ importers: buffer: specifier: ^6.0.3 version: 6.0.3 + ms: + specifier: ^2.1.3 + version: 2.1.3 devDependencies: '@simplewebauthn/types': specifier: ^10.0.0 version: 10.0.0 + '@types/ms': + specifier: ^0.7.34 + version: 0.7.34 '@types/node': specifier: ^22.1.0 version: 22.8.0 @@ -10400,6 +10406,11 @@ packages: peerDependencies: vue: ^3.2.0 + vue-router@4.5.0: + resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} + peerDependencies: + vue: ^3.2.0 + vue@3.5.12: resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==} peerDependencies: @@ -13690,12 +13701,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@nrwl/devkit@19.8.0(nx@19.8.0(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.12)(typescript@5.6.2))(@swc/core@1.7.26(@swc/helpers@0.5.13)))': - dependencies: - '@nx/devkit': 19.8.0(nx@19.8.0(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.12)(typescript@5.6.2))(@swc/core@1.7.26(@swc/helpers@0.5.13))) - transitivePeerDependencies: - - nx - '@nrwl/devkit@19.8.0(nx@19.8.6(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.12)(typescript@5.6.2))(@swc/core@1.7.26(@swc/helpers@0.5.13)))': dependencies: '@nx/devkit': 19.8.0(nx@19.8.6(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.12)(typescript@5.6.2))(@swc/core@1.7.26(@swc/helpers@0.5.13))) @@ -14298,7 +14303,7 @@ snapshots: '@nx/devkit@19.8.0(nx@19.8.0(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.12)(typescript@5.6.2))(@swc/core@1.7.26(@swc/helpers@0.5.13)))': dependencies: - '@nrwl/devkit': 19.8.0(nx@19.8.0(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.12)(typescript@5.6.2))(@swc/core@1.7.26(@swc/helpers@0.5.13))) + '@nrwl/devkit': 19.8.0(nx@19.8.6(@swc-node/register@1.10.9(@swc/core@1.7.26(@swc/helpers@0.5.13))(@swc/types@0.1.12)(typescript@5.6.2))(@swc/core@1.7.26(@swc/helpers@0.5.13))) ejs: 3.1.10 enquirer: 2.3.6 ignore: 5.3.2 @@ -24098,7 +24103,7 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.12(typescript@5.6.2) - vue-router@4.4.5(vue@3.5.13(typescript@5.6.2)): + vue-router@4.5.0(vue@3.5.13(typescript@5.6.2)): dependencies: '@vue/devtools-api': 6.6.4 vue: 3.5.13(typescript@5.6.2)