diff --git a/.github/workflows/sdk-test.yml b/.github/workflows/sdk-test.yml index 702e6326e..6ad7e67c1 100644 --- a/.github/workflows/sdk-test.yml +++ b/.github/workflows/sdk-test.yml @@ -28,6 +28,7 @@ jobs: - name: Test env: PRIVATE_KEY: ${{ secrets.TESTING_PRIVATE_KEY }} + PASSKEY_PRIVATE_KEY: ${{ secrets.TESTING_PASSKEY_PRIVATE_KEY }} run: yarn test - name: Upload coverage report diff --git a/guides/integrating-the-safe-core-sdk.md b/guides/integrating-the-safe-core-sdk.md index d02e28c10..1f0e38260 100644 --- a/guides/integrating-the-safe-core-sdk.md +++ b/guides/integrating-the-safe-core-sdk.md @@ -89,6 +89,7 @@ const contractNetworks: ContractNetworksConfig = { signMessageLibAddress: '', createCallAddress: '', simulateTxAccessorAddress: '', + safeWebAuthnSignerFactoryAddress:'', safeSingletonAbi: '', // Optional. Only needed with web3.js safeProxyFactoryAbi: '', // Optional. Only needed with web3.js multiSendAbi: '', // Optional. Only needed with web3.js @@ -97,6 +98,7 @@ const contractNetworks: ContractNetworksConfig = { signMessageLibAbi: '', // Optional. Only needed with web3.js createCallAbi: '', // Optional. Only needed with web3.js simulateTxAccessorAbi: '' // Optional. Only needed with web3.js + safeWebAuthnSignerFactoryAbi: '' // Optional. Only needed with web3.js } } diff --git a/packages/api-kit/package.json b/packages/api-kit/package.json index fc01dd207..0c8c4b67a 100644 --- a/packages/api-kit/package.json +++ b/packages/api-kit/package.json @@ -48,7 +48,7 @@ "@types/yargs": "^17.0.32", "chai": "^4.3.10", "chai-as-promised": "^7.1.1", - "hardhat": "^2.19.3", + "hardhat": "2.20.1", "mocha": "^10.2.0", "semver": "^7.6.1", "sinon": "^14.0.2", @@ -59,8 +59,8 @@ "yargs": "^17.7.2" }, "dependencies": { - "@safe-global/protocol-kit": "^4.0.4", - "@safe-global/safe-core-sdk-types": "^5.0.3", + "@safe-global/protocol-kit": "^4.1.0-alpha.2", + "@safe-global/safe-core-sdk-types": "^5.1.0-alpha.2", "ethers": "^6.13.1", "node-fetch": "^2.7.0" } diff --git a/packages/auth-kit/package.json b/packages/auth-kit/package.json index d2c818791..d3264fc6d 100644 --- a/packages/auth-kit/package.json +++ b/packages/auth-kit/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@safe-global/api-kit": "^2.4.4", - "@safe-global/protocol-kit": "^4.0.4", + "@safe-global/protocol-kit": "^4.1.0-alpha.2", "@web3auth/safeauth-embed": "^0.0.0", "ethers": "^6.13.1" } diff --git a/packages/onramp-kit/package.json b/packages/onramp-kit/package.json index 286476f1f..a0b6b4e17 100644 --- a/packages/onramp-kit/package.json +++ b/packages/onramp-kit/package.json @@ -37,8 +37,8 @@ "dependencies": { "@monerium/sdk": "^2.12.0", "@safe-global/api-kit": "^2.4.4", - "@safe-global/protocol-kit": "^4.0.4", - "@safe-global/safe-core-sdk-types": "^5.0.3", + "@safe-global/protocol-kit": "^4.1.0-alpha.2", + "@safe-global/safe-core-sdk-types": "^5.1.0-alpha.2", "@stripe/crypto": "^0.0.4", "@stripe/stripe-js": "^1.54.2", "ethers": "^6.13.1" diff --git a/packages/protocol-kit/contracts/Deps_V1_3_0.sol b/packages/protocol-kit/contracts/Deps_V1_3_0.sol index afbd1fbf9..7dc0aca90 100644 --- a/packages/protocol-kit/contracts/Deps_V1_3_0.sol +++ b/packages/protocol-kit/contracts/Deps_V1_3_0.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.7.0 <0.9.0; +pragma solidity >=0.7.0 <=0.8.0; import { GnosisSafeProxyFactory } from "@gnosis.pm/safe-contracts-v1.3.0/contracts/proxies/GnosisSafeProxyFactory.sol"; import { GnosisSafe } from "@gnosis.pm/safe-contracts-v1.3.0/contracts/GnosisSafe.sol"; diff --git a/packages/protocol-kit/contracts/Deps_passkeys.sol b/packages/protocol-kit/contracts/Deps_passkeys.sol new file mode 100644 index 000000000..ad204f89c --- /dev/null +++ b/packages/protocol-kit/contracts/Deps_passkeys.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.24; + +import {SafeWebAuthnSignerFactory} from '@safe-global/safe-passkey/contracts/SafeWebAuthnSignerFactory.sol'; +import {FCLP256Verifier} from '@safe-global/safe-passkey/contracts/verifiers/FCLP256Verifier.sol'; +import {SignatureValidator} from '@safe-global/safe-passkey/contracts/base/SignatureValidator.sol'; +import {P256, WebAuthn} from '@safe-global/safe-passkey/contracts/libraries/WebAuthn.sol'; + +/** + * @title Safe Smart Account + * @dev Minimal interface of a Safe smart account. This only includes functions that are used by + * this project. + * @custom:security-contact bounty@safe.global + */ +interface ISafeTest { + /** + * @notice Sets an initial storage of the Safe contract. + * @dev This method can only be called once. If a proxy was created without setting up, anyone + * can call setup and claim the proxy. + * @param owners List of Safe owners. + * @param threshold Number of required confirmations for a Safe transaction. + * @param to Contract address for optional delegate call. + * @param data Data payload for optional delegate call. + * @param fallbackHandler Handler for fallback calls to this contract + * @param paymentToken Token that should be used for the payment (0 is ETH) + * @param payment Value that should be paid + * @param paymentReceiver Address that should receive the payment (or 0 if tx.origin) + */ + function setup( + address[] calldata owners, + uint256 threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address payable paymentReceiver + ) external; + + /** + * @notice Reads `length` bytes of storage in the currents contract + * @param offset - the offset in the current contract's storage in words to start reading from + * @param length - the number of words (32 bytes) of data to read + * @return the bytes that were read. + */ + function getStorageAt(uint256 offset, uint256 length) external view returns (bytes memory); +} + +contract SafeWebAuthnSignerFactory_SV1_4_1 is SafeWebAuthnSignerFactory {} + +contract WebAuthnContract is FCLP256Verifier {} + +// TODO: ADD SHARED SIGNER CONTRACT FROM @safe-global/safe-passkey + +/** + * @title Safe WebAuthn Shared Signer + * @dev A contract for verifying WebAuthn signatures shared by all Safe accounts. This contract uses + * storage from the Safe account itself for full ERC-4337 compatibility. + */ +contract SafeWebAuthnSharedSigner is SignatureValidator { + /** + * @notice Data associated with a WebAuthn signer. It represents the X and Y coordinates of the + * signer's public key as well as the P256 verifiers to use. This is stored in account storage + * starting at the storage slot {SIGNER_SLOT}. + */ + struct Signer { + uint256 x; + uint256 y; + P256.Verifiers verifiers; + } + + /** + * @notice The storage slot of the mapping from shared WebAuthn signer address to signer data. + * @custom:computed-as keccak256("SafeWebAuthnSharedSigner.signer") - 1 + * @dev This value is intentionally computed to be a hash -1 as a precaution to avoid any + * potential issues from unintended hash collisions, and have enough space for all the signer + * fields. Also, this is the slot of a `mapping(address self => Signer)` to ensure that multiple + * {SafeWebAuthnSharedSigner} instances can coexist with the same account. + */ + uint256 private constant _SIGNER_MAPPING_SLOT = + 0x2e0aed53485dc2290ceb5ce14725558ad3e3a09d38c69042410ad15c2b4ea4e8; + + /** + * @notice An error indicating a `CALL` to a function that should only be `DELEGATECALL`-ed. + */ + error NotDelegateCalled(); + + /** + * @notice Address of the shared signer contract itself. + * @dev This is used for determining whether or not the contract is being `DELEGATECALL`-ed when + * setting signer data. + */ + address private immutable _SELF; + + /** + * @notice The starting storage slot on the account containing the signer data. + */ + uint256 public immutable SIGNER_SLOT; + + /** + * @notice Create a new shared WebAuthn signer instance. + */ + constructor() { + _SELF = address(this); + SIGNER_SLOT = uint256(keccak256(abi.encode(address(this), _SIGNER_MAPPING_SLOT))); + } + + /** + * @notice Validates the call is done via `DELEGATECALL`. + */ + modifier onlyDelegateCall() { + if (address(this) == _SELF) { + revert NotDelegateCalled(); + } + _; + } + + /** + * @notice Return the signer configuration for the specified account. + * @dev The calling account must be a Safe, as the signer data is stored in the Safe's storage + * and must be read with the {StorageAccessible} support from the Safe. + * @param account The account to request signer data for. + */ + function getConfiguration(address account) public view returns (Signer memory signer) { + bytes memory getStorageAtData = abi.encodeCall( + ISafeTest(account).getStorageAt, + (SIGNER_SLOT, 3) + ); + + // Call the {StorageAccessible.getStorageAt} with assembly. This allows us to return a + // zeroed out signer configuration instead of reverting for `account`s that are not Safes. + // We also, expect the implementation to behave **exactly** like the Safe's - that is it + // should encode the return data using a standard ABI encoding: + // - The first 32 bytes is the offset of the values bytes array, always `0x20` + // - The second 32 bytes is the length of the values bytes array, always `0x60` + // - the following 3 words (96 bytes) are the values of the signer configuration. + + // solhint-disable-next-line no-inline-assembly + assembly ('memory-safe') { + // Note that Yul expressions are evaluated in reverse order, so the `staticcall` is the + // first thing to be evaluated in the nested `and` expression. + if and( + and( + // The offset of the ABI encoded bytes is 0x20, this should always be the case + // for standard ABI encoding of `(bytes)` tuple that `getStorageAt` returns. + eq(mload(0x00), 0x20), + // The length of the encoded bytes is exactly 0x60 bytes (i.e. 3 words, which is + // exactly how much we read from the Safe's storage in the `getStorageAt` call). + eq(mload(0x20), 0x60) + ), + and( + // The length of the return data should be exactly 0xa0 bytes, which should + // always be the case for the Safe's `getStorageAt` implementation. + eq(returndatasize(), 0xa0), + // The call succeeded. We write the first two words of the return data into the + // scratch space, as we need to inspect them before copying the signer + // signer configuration to our `signer` memory pointer. + staticcall( + gas(), + account, + add(getStorageAtData, 0x20), + mload(getStorageAtData), + 0x00, + 0x40 + ) + ) + ) { + // Copy only the storage values from the return data to our `signer` memory address. + // This only happens on success, so the `signer` value will be zeroed out if any of + // the above conditions fail, indicating that no signer is configured. + returndatacopy(signer, 0x40, 0x60) + } + } + } + + /** + * @notice Sets the signer configuration for the calling account. + * @dev The Safe must call this function with a `DELEGATECALL`, as the signer data is stored in + * the Safe account's storage. + * @param signer The new signer data to set for the calling account. + */ + function configure(Signer memory signer) external onlyDelegateCall { + uint256 signerSlot = SIGNER_SLOT; + Signer storage signerStorage; + + // solhint-disable-next-line no-inline-assembly + assembly ('memory-safe') { + signerStorage.slot := signerSlot + } + + signerStorage.x = signer.x; + signerStorage.y = signer.y; + signerStorage.verifiers = signer.verifiers; + } + + /** + * @inheritdoc SignatureValidator + */ + function _verifySignature( + bytes32 message, + bytes calldata signature + ) internal view virtual override returns (bool isValid) { + Signer memory signer = getConfiguration(msg.sender); + + // Make sure that the signer is configured in the first place. + if (P256.Verifiers.unwrap(signer.verifiers) == 0) { + return false; + } + + isValid = WebAuthn.verifySignature( + message, + signature, + WebAuthn.USER_VERIFICATION, + signer.x, + signer.y, + signer.verifiers + ); + } +} diff --git a/packages/protocol-kit/hardhat.config.ts b/packages/protocol-kit/hardhat.config.ts index 1d0aa7a5e..228e6b0a6 100644 --- a/packages/protocol-kit/hardhat.config.ts +++ b/packages/protocol-kit/hardhat.config.ts @@ -31,7 +31,27 @@ if (PK) { const config: HardhatUserConfig = { defaultNetwork: 'hardhat', solidity: { - compilers: [{ version: '0.5.17' }, { version: '0.5.3' }, { version: '0.8.0' }] + compilers: [ + { version: '0.5.17' }, + { version: '0.5.3' }, + { version: '0.8.0' }, + { + version: '0.8.24', + settings: { + optimizer: { + enabled: true, + runs: 10_000_000 + }, + viaIR: false, + evmVersion: 'paris' + } + } + ], + overrides: { + '@gnosis.pm/safe-contracts-v1.3.0/contracts/handler/CompatibilityFallbackHandler.sol': { + version: '0.8.0' + } + } }, paths: { artifacts: 'artifacts', diff --git a/packages/protocol-kit/hardhat/deploy/deploy-contracts.ts b/packages/protocol-kit/hardhat/deploy/deploy-contracts.ts index 28789b6e7..93171e1c5 100644 --- a/packages/protocol-kit/hardhat/deploy/deploy-contracts.ts +++ b/packages/protocol-kit/hardhat/deploy/deploy-contracts.ts @@ -70,6 +70,22 @@ const simulateTxAccessorContracts: SafeVersions = { '1.0.0': { name: 'SimulateTxAccessor_SV1_3_0' } } +const safeWebAuthnSignerFactoryContracts: SafeVersions = { + '1.4.1': { name: 'SafeWebAuthnSignerFactory_SV1_4_1' }, + '1.3.0': { name: 'SafeWebAuthnSignerFactory_SV1_4_1' }, + '1.2.0': { name: 'SafeWebAuthnSignerFactory_SV1_4_1' }, + '1.1.1': { name: 'SafeWebAuthnSignerFactory_SV1_4_1' }, + '1.0.0': { name: 'SafeWebAuthnSignerFactory_SV1_4_1' } +} + +const safeWebAuthnSharedSignerContracts: SafeVersions = { + '1.4.1': { name: 'SafeWebAuthnSharedSigner' }, + '1.3.0': { name: 'SafeWebAuthnSharedSigner' }, + '1.2.0': { name: 'SafeWebAuthnSharedSigner' }, + '1.1.1': { name: 'SafeWebAuthnSharedSigner' }, + '1.0.0': { name: 'SafeWebAuthnSharedSigner' } +} + export const safeDeployed = safeContracts[safeVersionDeployed] export const proxyFactoryDeployed = proxyFactoryContracts[safeVersionDeployed] export const multiSendDeployed = multiSendContracts[safeVersionDeployed] @@ -79,6 +95,10 @@ export const compatibilityFallbackHandlerDeployed = export const signMessageLibDeployed = signMessageLibContracts[safeVersionDeployed] export const createCallDeployed = createCallContracts[safeVersionDeployed] export const simulateTxAccessorDeployed = simulateTxAccessorContracts[safeVersionDeployed] +export const safeWebAuthnSignerFactoryDeployed = + safeWebAuthnSignerFactoryContracts[safeVersionDeployed] +export const safeWebAuthnSharedSignerDeployed = + safeWebAuthnSharedSignerContracts[safeVersionDeployed] const deploy: DeployFunction = async (hre: HardhatRuntimeEnvironment): Promise => { const { deployments, getNamedAccounts } = hre @@ -141,6 +161,27 @@ const deploy: DeployFunction = async (hre: HardhatRuntimeEnvironment): Promise { + const isPasskeySigner = await this.#safeProvider.isPasskeySigner() + const signerAddress = await this.#safeProvider.getSignerAddress() + + if (isPasskeySigner && signerAddress) { + let signature = await this.#safeProvider.signMessage(hash) + + signature = adjustVInSignature(SigningMethod.ETH_SIGN, signature, hash, signerAddress) + + const safeSignature = new EthSafeSignature(signerAddress, signature, true) + + return safeSignature + } + const signature = await generateSignature(this.#safeProvider, hash) return signature @@ -746,7 +787,13 @@ class Safe { let signature: SafeSignature - if (signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA_V4) { + const isPasskeySigner = await this.#safeProvider.isPasskeySigner() + + if (isPasskeySigner) { + const txHash = await this.getTransactionHash(transaction) + + signature = await this.signHash(txHash) + } else if (signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA_V4) { signature = await this.signTypedData(transaction, 'v4') } else if (signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA_V3) { signature = await this.signTypedData(transaction, 'v3') @@ -818,7 +865,6 @@ class Safe { throw new Error('Transaction hashes can only be approved by Safe owners') } - // TODO: fix this return this.#contractManager.safeContract.approveHash(hash, { from: signerAddress, ...options @@ -1007,16 +1053,38 @@ class Safe { * @throws "Threshold cannot exceed owner count" */ async createAddOwnerTx( - { ownerAddress, threshold }: AddOwnerTxParams, + params: AddOwnerTxParams | AddPasskeyOwnerTxParams, options?: SafeTransactionOptionalProps ): Promise { - const safeTransactionData = { + const isPasskey = isPasskeyParam(params) + + const ownerAddress = isPasskey + ? await getPasskeyOwnerAddress(this, params.passkey) + : params.ownerAddress + + const { threshold } = params + + const addOwnerTransaction = { to: await this.getAddress(), value: '0', data: await this.#ownerManager.encodeAddOwnerWithThresholdData(ownerAddress, threshold) } + + const transactions = [addOwnerTransaction] + + // The passkey Signer is a contract compliant with EIP-1271 standards, we need to check if it has been deployed. + if (isPasskey && !(await this.#safeProvider.isContractDeployed(ownerAddress))) { + // If it has not been deployed, we need to create a batch that includes both the Signer contract deployment and the addOwner transaction + const passkeyDeploymentTransaction = await createPasskeyDeploymentTransaction( + this, + params.passkey + ) + + transactions.push(passkeyDeploymentTransaction) + } + const safeTransaction = await this.createTransaction({ - transactions: [safeTransactionData], + transactions, options }) return safeTransaction @@ -1034,9 +1102,17 @@ class Safe { * @throws "Threshold cannot exceed owner count" */ async createRemoveOwnerTx( - { ownerAddress, threshold }: RemoveOwnerTxParams, + params: RemoveOwnerTxParams | RemovePasskeyOwnerTxParams, options?: SafeTransactionOptionalProps ): Promise { + const { threshold } = params + + const isPasskey = isPasskeyParam(params) + + const ownerAddress = isPasskey + ? await getPasskeyOwnerAddress(this, params.passkey) + : params.ownerAddress + const safeTransactionData = { to: await this.getAddress(), value: '0', @@ -1061,18 +1137,44 @@ class Safe { * @throws "Old address provided is not an owner" */ async createSwapOwnerTx( - { oldOwnerAddress, newOwnerAddress }: SwapOwnerTxParams, + params: SwapOwnerTxParams, options?: SafeTransactionOptionalProps ): Promise { - const safeTransactionData = { + const oldOwnerAddress = isOldOwnerPasskey(params) + ? await getPasskeyOwnerAddress(this, params.oldOwnerPasskey) + : params.oldOwnerAddress + + const newOwnerAddress = isNewOwnerPasskey(params) + ? await getPasskeyOwnerAddress(this, params.newOwnerPasskey) + : params.newOwnerAddress + + const swapOwnerTransaction = { to: await this.getAddress(), value: '0', data: await this.#ownerManager.encodeSwapOwnerData(oldOwnerAddress, newOwnerAddress) } + + const transactions = [swapOwnerTransaction] + + // The passkey Signer is a contract compliant with EIP-1271 standards, we need to check if it has been deployed. + if ( + isNewOwnerPasskey(params) && + !(await this.#safeProvider.isContractDeployed(newOwnerAddress)) + ) { + // If it has not been deployed, we need to create a batch that includes both the Signer contract deployment and the addOwner transaction + const passkeyDeploymentTransaction = await createPasskeyDeploymentTransaction( + this, + params.newOwnerPasskey + ) + + transactions.push(passkeyDeploymentTransaction) + } + const safeTransaction = await this.createTransaction({ - transactions: [safeTransactionData], + transactions, options }) + return safeTransaction } diff --git a/packages/protocol-kit/src/SafeFactory.ts b/packages/protocol-kit/src/SafeFactory.ts index 180170e4f..d50c093fd 100644 --- a/packages/protocol-kit/src/SafeFactory.ts +++ b/packages/protocol-kit/src/SafeFactory.ts @@ -62,7 +62,7 @@ class SafeFactory { }: SafeFactoryInitConfig) { this.#provider = provider this.#signer = signer - this.#safeProvider = new SafeProvider({ provider, signer }) + this.#safeProvider = await SafeProvider.init(provider, signer, safeVersion, contractNetworks) this.#safeVersion = safeVersion this.#isL1SafeSingleton = isL1SafeSingleton this.#contractNetworks = contractNetworks diff --git a/packages/protocol-kit/src/SafeProvider.ts b/packages/protocol-kit/src/SafeProvider.ts index 7b79fde1d..bef5784e9 100644 --- a/packages/protocol-kit/src/SafeProvider.ts +++ b/packages/protocol-kit/src/SafeProvider.ts @@ -6,15 +6,25 @@ import { BrowserProvider, JsonRpcProvider } from 'ethers' -import { generateTypedData, validateEip3770Address } from '@safe-global/protocol-kit/utils' +import { + SAFE_FEATURES, + generateTypedData, + hasSafeFeature, + validateEip3770Address +} from '@safe-global/protocol-kit/utils' import { isTypedDataSigner } from '@safe-global/protocol-kit/contracts/utils' +import { + getSafeWebAuthnSharedSignerContract, + getSafeWebAuthnSignerFactoryContract +} from '@safe-global/protocol-kit/contracts/safeDeploymentContracts' import { EMPTY_DATA } from '@safe-global/protocol-kit/utils/constants' import { EIP712TypedDataMessage, EIP712TypedDataTx, Eip3770Address, - SafeEIP712Args + SafeEIP712Args, + SafeVersion } from '@safe-global/safe-core-sdk-types' import { getCompatibilityFallbackHandlerContractInstance, @@ -23,6 +33,8 @@ import { getMultiSendContractInstance, getSafeContractInstance, getSafeProxyFactoryContractInstance, + getSafeWebAuthnSharedSignerContractInstance, + getSafeWebAuthnSignerFactoryContractInstance, getSignMessageLibContractInstance, getSimulateTxAccessorContractInstance } from './contracts/contractInstances' @@ -30,17 +42,25 @@ import { SafeProviderTransaction, GetContractProps, SafeProviderConfig, - Eip1193Provider, - HttpTransport, - SocketTransport + SafeSigner, + SafeConfig, + ContractNetworksConfig } from '@safe-global/protocol-kit/types' +import PasskeySigner from './utils/passkeys/PasskeySigner' +import { DEFAULT_SAFE_VERSION } from './contracts/config' class SafeProvider { #externalProvider: BrowserProvider | JsonRpcProvider - signer?: string - provider: Eip1193Provider | HttpTransport | SocketTransport - - constructor({ provider, signer }: SafeProviderConfig) { + provider: SafeProviderConfig['provider'] + signer?: SafeSigner + + constructor({ + provider, + signer + }: { + provider: SafeProviderConfig['provider'] + signer?: SafeSigner + }) { if (typeof provider === 'string') { this.#externalProvider = new JsonRpcProvider(provider) } else { @@ -55,15 +75,85 @@ class SafeProvider { return this.#externalProvider } - async getExternalSigner(): Promise { - // If the signer is not an Ethereum address, it should be a private key - if (this.signer && !ethers.isAddress(this.signer)) { - const privateKeySigner = new ethers.Wallet(this.signer, this.#externalProvider) - return privateKeySigner + static async init( + provider: SafeConfig['provider'], + signer?: SafeConfig['signer'], + safeVersion: SafeVersion = DEFAULT_SAFE_VERSION, + contractNetworks?: ContractNetworksConfig, + safeAddress?: string, + owners?: string[] + ): Promise { + const isPasskeySigner = signer && typeof signer !== 'string' + + if (isPasskeySigner) { + if (!hasSafeFeature(SAFE_FEATURES.PASSKEY_SIGNER, safeVersion)) { + throw new Error( + 'Current version of the Safe does not support the Passkey signer functionality' + ) + } + + const safeProvider = new SafeProvider({ + provider + }) + const chainId = await safeProvider.getChainId() + const customContracts = contractNetworks?.[chainId.toString()] + + let passkeySigner + const isPasskeySignerConfig = !(signer instanceof PasskeySigner) + + if (isPasskeySignerConfig) { + // signer is type PasskeyArgType {rawId, coordinates, customVerifierAddress? } + const safeWebAuthnSignerFactoryContract = await getSafeWebAuthnSignerFactoryContract({ + safeProvider, + safeVersion, + customContracts + }) + + const safeWebAuthnSharedSignerContract = await getSafeWebAuthnSharedSignerContract({ + safeProvider, + safeVersion, + customContracts + }) + + passkeySigner = await PasskeySigner.init( + signer, + safeWebAuthnSignerFactoryContract, + safeWebAuthnSharedSignerContract, + safeProvider.getExternalProvider(), + safeAddress || '', + owners || [], + chainId.toString() + ) + } else { + // signer was already initialized and we pass a PasskeySigner instance (reconnecting) + passkeySigner = signer + } + + return new SafeProvider({ + provider, + signer: passkeySigner + }) + } else { + return new SafeProvider({ + provider, + signer + }) } + } + + async getExternalSigner(): Promise { + if (typeof this.signer === 'string') { + // If the signer is not an Ethereum address, it should be a private key + if (!ethers.isAddress(this.signer)) { + const privateKeySigner = new ethers.Wallet(this.signer, this.#externalProvider) + return privateKeySigner + } - if (this.signer) { return this.#externalProvider.getSigner(this.signer) + } else { + if (this.signer) { + return this.signer + } } if (this.#externalProvider instanceof BrowserProvider) { @@ -73,6 +163,12 @@ class SafeProvider { return undefined } + async isPasskeySigner(): Promise { + const signer = (await this.getExternalSigner()) as PasskeySigner + + return signer && !!signer.passkeyRawId + } + isAddress(address: string): boolean { return ethers.isAddress(address) } @@ -201,6 +297,32 @@ class SafeProvider { ) } + async getSafeWebAuthnSignerFactoryContract({ + safeVersion, + customContractAddress, + customContractAbi + }: GetContractProps) { + return getSafeWebAuthnSignerFactoryContractInstance( + safeVersion, + this, + customContractAddress, + customContractAbi + ) + } + + async getSafeWebAuthnSharedSignerContract({ + safeVersion, + customContractAddress, + customContractAbi + }: GetContractProps) { + return getSafeWebAuthnSharedSignerContractInstance( + safeVersion, + this, + customContractAddress, + customContractAbi + ) + } + async getContractCode(address: string, blockTag?: string | number): Promise { return this.#externalProvider.getCode(address, blockTag) } diff --git a/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts b/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts new file mode 100644 index 000000000..433d8e700 --- /dev/null +++ b/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts @@ -0,0 +1,70 @@ +import { Abi } from 'abitype' +import { ContractRunner, InterfaceAbi } from 'ethers' +import SafeProvider from '@safe-global/protocol-kit/SafeProvider' +import BaseContract from '@safe-global/protocol-kit/contracts/BaseContract' +import { + SafeVersion, + TransactionOptions, + CreateProxyProps as CreateProxyPropsGeneral +} from '@safe-global/safe-core-sdk-types' +import { contractName } from '@safe-global/protocol-kit/contracts/config' + +export interface CreateProxyProps extends CreateProxyPropsGeneral { + options?: TransactionOptions +} + +/** + * Abstract class SafeWebAuthnSharedSignerBaseContract extends BaseContract to specifically integrate with the SafeWebAuthnSharedSigner contract. + * It is designed to be instantiated for different versions of the Safe contract. + * + * Subclasses of SafeWebAuthnSharedSignerBaseContract are expected to represent specific versions of the contract. + * + * @template SafeWebAuthnSharedSignerContractAbiType - The ABI type specific to the version of the Safe WebAuthn Shared Signer contract, extending InterfaceAbi from Ethers. + * @extends BaseContract - Extends the generic BaseContract. + * + * Example subclasses: + * - SafeWebAuthnSharedSignerContract_v0_2_1 extends SafeWebAuthnSharedSignerBaseContract + */ +abstract class SafeWebAuthnSharedSignerBaseContract< + SafeWebAuthnSharedSignerContractAbiType extends InterfaceAbi & Abi +> extends BaseContract { + contractName: contractName + + /** + * @constructor + * Constructs an instance of SafeWebAuthnSharedSignerBaseContract. + * + * @param chainId - The chain ID of the contract. + * @param safeProvider - An instance of SafeProvider. + * @param defaultAbi - The default ABI for the Safe contract. It should be compatible with the specific version of the contract. + * @param safeVersion - The version of the Safe contract. + * @param customContractAddress - Optional custom address for the contract. If not provided, the address is derived from the Safe deployments based on the chainId and safeVersion. + * @param customContractAbi - Optional custom ABI for the contract. If not provided, the ABI is derived from the Safe deployments or the defaultAbi is used. + */ + constructor( + chainId: bigint, + safeProvider: SafeProvider, + defaultAbi: SafeWebAuthnSharedSignerContractAbiType, + safeVersion: SafeVersion, + customContractAddress?: string, + customContractAbi?: SafeWebAuthnSharedSignerContractAbiType, + runner?: ContractRunner | null + ) { + const contractName = 'safeWebAuthnSharedSignerVersion' + + super( + contractName, + chainId, + safeProvider, + defaultAbi, + safeVersion, + customContractAddress, + customContractAbi, + runner + ) + + this.contractName = contractName + } +} + +export default SafeWebAuthnSharedSignerBaseContract diff --git a/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/v0.2.1/SafeWebAuthnSharedSignerContract_v0_2_1.ts b/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/v0.2.1/SafeWebAuthnSharedSignerContract_v0_2_1.ts new file mode 100644 index 000000000..3efecb836 --- /dev/null +++ b/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/v0.2.1/SafeWebAuthnSharedSignerContract_v0_2_1.ts @@ -0,0 +1,83 @@ +import SafeWebAuthnSharedSignerBaseContract from '@safe-global/protocol-kit/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract' +import { + SafeVersion, + SafeWebAuthnSharedSignerContract_v0_2_1_Abi, + SafeWebAuthnSharedSignerContract_v0_2_1_Contract, + SafeWebAuthnSharedSignerContract_v0_2_1_Function, + SafeWebAuthnSharedSigner_0_2_1_ContractArtifacts +} from '@safe-global/safe-core-sdk-types' +import SafeProvider from '@safe-global/protocol-kit/SafeProvider' + +/** + * SafeWebAuthnSharedSignerContract_v0_2_1 is the implementation specific to the SafeWebAuthnSharedSigner contract version 0.2.1. + * + * This class specializes in handling interactions with the SafeWebAuthnSharedSigner contract version 0.2.1 using Ethers.js v6. + * + * @extends SafeWebAuthnSharedSignerBaseContract - Inherits from SafeWebAuthnSharedSignerBaseContract with ABI specific to SafeWebAuthnSigner Factory contract version 0.2.1. + * @implements SafeWebAuthnSharedSignerContract_v0_2_1_Contract - Implements the interface specific to SafeWebAuthnSharedSigner contract version 0.2.1. + */ +class SafeWebAuthnSharedSignerContract_v0_2_1 + extends SafeWebAuthnSharedSignerBaseContract + implements SafeWebAuthnSharedSignerContract_v0_2_1_Contract +{ + safeVersion: SafeVersion + + /** + * Constructs an instance of SafeWebAuthnSharedSignerContract_v0_2_1 + * + * @param chainId - The chain ID where the contract resides. + * @param safeProvider - An instance of SafeProvider. + * @param safeVersion - The version of the Safe contract. + * @param customContractAddress - Optional custom address for the contract. If not provided, the address is derived from the Safe deployments based on the chainId and safeVersion. + * @param customContractAbi - Optional custom ABI for the contract. If not provided, the default ABI for version 0.2.1 is used. + */ + constructor( + chainId: bigint, + safeProvider: SafeProvider, + safeVersion: SafeVersion, + customContractAddress?: string, + customContractAbi?: SafeWebAuthnSharedSignerContract_v0_2_1_Abi + ) { + const defaultAbi = SafeWebAuthnSharedSigner_0_2_1_ContractArtifacts.abi + + super(chainId, safeProvider, defaultAbi, safeVersion, customContractAddress, customContractAbi) + + this.safeVersion = safeVersion + } + + /** + * Return the signer configuration for the specified account. + * @param args - Array[address] + * @returns Array[signer] + */ + getConfiguration: SafeWebAuthnSharedSignerContract_v0_2_1_Function<'getConfiguration'> = async ( + args + ) => { + return [await this.contract.getConfiguration(...args)] + } + + /** + * Sets the signer configuration for the calling account. + * @param args - Array[signer] + * @returns Array[] + */ + configure: SafeWebAuthnSharedSignerContract_v0_2_1_Function<'configure'> = async (args) => { + await this.contract.configure(...args) + return [] + } + + isValidSignature: SafeWebAuthnSharedSignerContract_v0_2_1_Function<'isValidSignature'> = async ( + args + ) => { + return [await this.contract.isValidSignature(...args)] + } + + /** + * @returns The starting storage slot on the account containing the signer data. + */ + SIGNER_SLOT: SafeWebAuthnSharedSignerContract_v0_2_1_Function<'SIGNER_SLOT'> = async () => { + return [await this.contract.SIGNER_SLOT()] + } +} + +export default SafeWebAuthnSharedSignerContract_v0_2_1 diff --git a/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts b/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts new file mode 100644 index 000000000..17f604548 --- /dev/null +++ b/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts @@ -0,0 +1,70 @@ +import { Abi } from 'abitype' +import { ContractRunner, InterfaceAbi } from 'ethers' +import SafeProvider from '@safe-global/protocol-kit/SafeProvider' +import BaseContract from '@safe-global/protocol-kit/contracts/BaseContract' +import { + SafeVersion, + TransactionOptions, + CreateProxyProps as CreateProxyPropsGeneral +} from '@safe-global/safe-core-sdk-types' +import { contractName } from '@safe-global/protocol-kit/contracts/config' + +export interface CreateProxyProps extends CreateProxyPropsGeneral { + options?: TransactionOptions +} + +/** + * Abstract class SafeWebAuthnSignerFactoryBaseContract extends BaseContract to specifically integrate with the SafeWebAuthnSignerFactory contract. + * It is designed to be instantiated for different versions of the Safe contract. + * + * Subclasses of SafeWebAuthnSignerFactoryBaseContract are expected to represent specific versions of the contract. + * + * @template SafeWebAuthnSignerFactoryContractAbiType - The ABI type specific to the version of the Safe WebAuthn Signer Factory contract, extending InterfaceAbi from Ethers. + * @extends BaseContract - Extends the generic BaseContract. + * + * Example subclasses: + * - SafeWebAuthnSignerFactoryContract_v0_2_1 extends SafeWebAuthnSignerFactoryBaseContract + */ +abstract class SafeWebAuthnSignerFactoryBaseContract< + SafeWebAuthnSignerFactoryContractAbiType extends InterfaceAbi & Abi +> extends BaseContract { + contractName: contractName + + /** + * @constructor + * Constructs an instance of SafeWebAuthnSignerFactoryBaseContract. + * + * @param chainId - The chain ID of the contract. + * @param safeProvider - An instance of SafeProvider. + * @param defaultAbi - The default ABI for the Safe contract. It should be compatible with the specific version of the contract. + * @param safeVersion - The version of the Safe contract. + * @param customContractAddress - Optional custom address for the contract. If not provided, the address is derived from the Safe deployments based on the chainId and safeVersion. + * @param customContractAbi - Optional custom ABI for the contract. If not provided, the ABI is derived from the Safe deployments or the defaultAbi is used. + */ + constructor( + chainId: bigint, + safeProvider: SafeProvider, + defaultAbi: SafeWebAuthnSignerFactoryContractAbiType, + safeVersion: SafeVersion, + customContractAddress?: string, + customContractAbi?: SafeWebAuthnSignerFactoryContractAbiType, + runner?: ContractRunner | null + ) { + const contractName = 'safeWebAuthnSignerFactoryVersion' + + super( + contractName, + chainId, + safeProvider, + defaultAbi, + safeVersion, + customContractAddress, + customContractAbi, + runner + ) + + this.contractName = contractName + } +} + +export default SafeWebAuthnSignerFactoryBaseContract diff --git a/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/v0.2.1/SafeWebAuthnSignerFactoryContract_v0_2_1.ts b/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/v0.2.1/SafeWebAuthnSignerFactoryContract_v0_2_1.ts new file mode 100644 index 000000000..da8a15f95 --- /dev/null +++ b/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/v0.2.1/SafeWebAuthnSignerFactoryContract_v0_2_1.ts @@ -0,0 +1,74 @@ +import SafeWebAuthnSignerFactoryBaseContract from '@safe-global/protocol-kit/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract' +import { + SafeVersion, + SafeWebAuthnSignerFactoryContract_v0_2_1_Abi, + SafeWebAuthnSignerFactoryContract_v0_2_1_Contract, + SafeWebAuthnSignerFactoryContract_v0_2_1_Function, + SafeWebAuthnSignerFactory_0_2_1_ContractArtifacts +} from '@safe-global/safe-core-sdk-types' +import SafeProvider from '@safe-global/protocol-kit/SafeProvider' + +/** + * SafeWebAuthnSignerFactoryContract_v0_2_1 is the implementation specific to the SafeWebAuthnSigner Factory contract version 0.2.1. + * + * This class specializes in handling interactions with the SafeWebAuthnSigner Factory contract version 0.2.1 using Ethers.js v6. + * + * @extends SafeWebAuthnSignerFactoryBaseContract - Inherits from SafeWebAuthnSignerFactoryBaseContract with ABI specific to SafeWebAuthnSigner Factory contract version 0.2.1. + * @implements SafeWebAuthnSignerFactoryContract_v0_2_1_Contract - Implements the interface specific to SafeWebAuthnSigner Factory contract version 0.2.1. + */ +class SafeWebAuthnSignerFactoryContract_v0_2_1 + extends SafeWebAuthnSignerFactoryBaseContract + implements SafeWebAuthnSignerFactoryContract_v0_2_1_Contract +{ + safeVersion: SafeVersion + + /** + * Constructs an instance of SafeWebAuthnSignerFactoryContract_v0_2_1 + * + * @param chainId - The chain ID where the contract resides. + * @param safeProvider - An instance of SafeProvider. + * @param safeVersion - The version of the Safe contract. + * @param customContractAddress - Optional custom address for the contract. If not provided, the address is derived from the Safe deployments based on the chainId and safeVersion. + * @param customContractAbi - Optional custom ABI for the contract. If not provided, the default ABI for version 0.2.1 is used. + */ + constructor( + chainId: bigint, + safeProvider: SafeProvider, + safeVersion: SafeVersion, + customContractAddress?: string, + customContractAbi?: SafeWebAuthnSignerFactoryContract_v0_2_1_Abi + ) { + const defaultAbi = SafeWebAuthnSignerFactory_0_2_1_ContractArtifacts.abi + + super(chainId, safeProvider, defaultAbi, safeVersion, customContractAddress, customContractAbi) + + this.safeVersion = safeVersion + } + + /** + * Returns the address of the Signer. + * @param args - Array[x, y, verifiers] + * @returns Array[signer] + */ + getSigner: SafeWebAuthnSignerFactoryContract_v0_2_1_Function<'getSigner'> = async (args) => { + return [await this.contract.getSigner(...args)] + } + + /** + * Returns the address of the Signer and deploy the signer contract if its not deployed yet. + * @param args - Array[x, y, verifiers] + * @returns Array[signer] + */ + createSigner: SafeWebAuthnSignerFactoryContract_v0_2_1_Function<'createSigner'> = async ( + args + ) => { + return [await this.contract.createSigner(...args)] + } + + isValidSignatureForSigner: SafeWebAuthnSignerFactoryContract_v0_2_1_Function<'isValidSignatureForSigner'> = + async (args) => { + return [await this.contract.isValidSignatureForSigner(...args)] + } +} + +export default SafeWebAuthnSignerFactoryContract_v0_2_1 diff --git a/packages/protocol-kit/src/contracts/config.ts b/packages/protocol-kit/src/contracts/config.ts index 585b62151..cb6fc26f1 100644 --- a/packages/protocol-kit/src/contracts/config.ts +++ b/packages/protocol-kit/src/contracts/config.ts @@ -11,6 +11,11 @@ import { getSignMessageLibDeployment, getSimulateTxAccessorDeployment } from '@safe-global/safe-deployments' +import { + Deployment, + getSafeWebAuthnSignerFactoryDeployment, + getSafeWebAuthnShareSignerDeployment +} from '@safe-global/safe-modules-deployments' import { SafeVersion } from '@safe-global/safe-core-sdk-types' export const DEFAULT_SAFE_VERSION: SafeVersion = '1.3.0' @@ -26,6 +31,8 @@ type contractNames = { signMessageLibVersion?: string createCallVersion?: string simulateTxAccessorVersion?: string + safeWebAuthnSignerFactoryVersion?: string + safeWebAuthnSharedSignerVersion?: string } type SafeDeploymentsVersions = Record @@ -42,7 +49,9 @@ export const safeDeploymentsVersions: SafeDeploymentsVersions = { multiSendCallOnlyVersion: '1.4.1', signMessageLibVersion: '1.4.1', createCallVersion: '1.4.1', - simulateTxAccessorVersion: '1.4.1' + simulateTxAccessorVersion: '1.4.1', + safeWebAuthnSignerFactoryVersion: '0.2.1', + safeWebAuthnSharedSignerVersion: '0.2.1' }, '1.3.0': { safeSingletonVersion: '1.3.0', @@ -53,7 +62,9 @@ export const safeDeploymentsVersions: SafeDeploymentsVersions = { multiSendCallOnlyVersion: '1.3.0', signMessageLibVersion: '1.3.0', createCallVersion: '1.3.0', - simulateTxAccessorVersion: '1.3.0' + simulateTxAccessorVersion: '1.3.0', + safeWebAuthnSignerFactoryVersion: '0.2.1', + safeWebAuthnSharedSignerVersion: '0.2.1' }, '1.2.0': { safeSingletonVersion: '1.2.0', @@ -63,7 +74,9 @@ export const safeDeploymentsVersions: SafeDeploymentsVersions = { multiSendVersion: '1.1.1', multiSendCallOnlyVersion: '1.3.0', signMessageLibVersion: '1.3.0', - createCallVersion: '1.3.0' + createCallVersion: '1.3.0', + safeWebAuthnSignerFactoryVersion: '0.2.1', + safeWebAuthnSharedSignerVersion: '0.2.1' }, '1.1.1': { safeSingletonVersion: '1.1.1', @@ -73,7 +86,9 @@ export const safeDeploymentsVersions: SafeDeploymentsVersions = { multiSendVersion: '1.1.1', multiSendCallOnlyVersion: '1.3.0', signMessageLibVersion: '1.3.0', - createCallVersion: '1.3.0' + createCallVersion: '1.3.0', + safeWebAuthnSignerFactoryVersion: '0.2.1', + safeWebAuthnSharedSignerVersion: '0.2.1' }, '1.0.0': { safeSingletonVersion: '1.0.0', @@ -83,7 +98,9 @@ export const safeDeploymentsVersions: SafeDeploymentsVersions = { multiSendVersion: '1.1.1', multiSendCallOnlyVersion: '1.3.0', signMessageLibVersion: '1.3.0', - createCallVersion: '1.3.0' + createCallVersion: '1.3.0', + safeWebAuthnSignerFactoryVersion: '0.2.1', + safeWebAuthnSharedSignerVersion: '0.2.1' } } @@ -93,7 +110,7 @@ export const safeDeploymentsL1ChainIds = [ const contractFunctions: Record< contractName, - (filter?: DeploymentFilter) => SingletonDeployment | undefined + (filter?: DeploymentFilter) => SingletonDeployment | undefined | Deployment > = { safeSingletonVersion: getSafeSingletonDeployment, safeSingletonL2Version: getSafeL2SingletonDeployment, @@ -103,7 +120,9 @@ const contractFunctions: Record< multiSendCallOnlyVersion: getMultiSendCallOnlyDeployment, signMessageLibVersion: getSignMessageLibDeployment, createCallVersion: getCreateCallDeployment, - simulateTxAccessorVersion: getSimulateTxAccessorDeployment + simulateTxAccessorVersion: getSimulateTxAccessorDeployment, + safeWebAuthnSignerFactoryVersion: getSafeWebAuthnSignerFactoryDeployment, + safeWebAuthnSharedSignerVersion: getSafeWebAuthnShareSignerDeployment } export function getContractDeployment( @@ -119,7 +138,7 @@ export function getContractDeployment( released: true } - const deployment = contractFunctions[contractName](filters) + const deployment = contractFunctions[contractName](filters) as SingletonDeployment return deployment } diff --git a/packages/protocol-kit/src/contracts/contractInstances.ts b/packages/protocol-kit/src/contracts/contractInstances.ts index 4021c9bfb..9430a6122 100644 --- a/packages/protocol-kit/src/contracts/contractInstances.ts +++ b/packages/protocol-kit/src/contracts/contractInstances.ts @@ -22,7 +22,9 @@ import { CreateCallContract_v1_4_1_Abi, CreateCallContract_v1_3_0_Abi, SimulateTxAccessorContract_v1_4_1_Abi, - SimulateTxAccessorContract_v1_3_0_Abi + SimulateTxAccessorContract_v1_3_0_Abi, + SafeWebAuthnSignerFactoryContract_v0_2_1_Abi, + SafeWebAuthnSharedSignerContract_v0_2_1_Abi } from '@safe-global/safe-core-sdk-types' import CreateCallContract_v1_3_0 from './CreateCall/v1.3.0/CreateCallContract_v1_3_0' import CreateCallContract_v1_4_1 from './CreateCall/v1.4.1/CreateCallContract_v1_4_1' @@ -46,6 +48,8 @@ import SimulateTxAccessorContract_v1_3_0 from './SimulateTxAccessor/v1.3.0/Simul import SimulateTxAccessorContract_v1_4_1 from './SimulateTxAccessor/v1.4.1/SimulateTxAccessorContract_v1_4_1' import CompatibilityFallbackHandlerContract_v1_3_0 from './CompatibilityFallbackHandler/v1.3.0/CompatibilityFallbackHandlerContract_v1_3_0' import CompatibilityFallbackHandlerContract_v1_4_1 from './CompatibilityFallbackHandler/v1.4.1/CompatibilityFallbackHandlerContract_v1_4_1' +import SafeWebAuthnSignerFactoryContract_v0_2_1 from './SafeWebAuthnSignerFactory/v0.2.1/SafeWebAuthnSignerFactoryContract_v0_2_1' +import SafeWebAuthnSharedSignerContract_v0_2_1 from './SafeWebAuthnSharedSigner/v0.2.1/SafeWebAuthnSharedSignerContract_v0_2_1' import SafeProvider from '../SafeProvider' export async function getSafeContractInstance( @@ -244,7 +248,6 @@ export async function getMultiSendCallOnlyContractInstance( export async function getSafeProxyFactoryContractInstance( safeVersion: SafeVersion, safeProvider: SafeProvider, - // TODO: remove this ?? signerOrProvider: AbstractSigner | Provider, contractAddress?: string, customContractAbi?: JsonFragment | JsonFragment[] | undefined @@ -411,3 +414,60 @@ export async function getSimulateTxAccessorContractInstance( return simulateTxAccessorContractInstance } + +export async function getSafeWebAuthnSignerFactoryContractInstance( + safeVersion: SafeVersion, + safeProvider: SafeProvider, + contractAddress?: string, + customContractAbi?: JsonFragment | JsonFragment[] | undefined +): Promise { + const chainId = await safeProvider.getChainId() + + switch (safeVersion) { + case '1.4.1': + case '1.3.0': + const safeWebAuthnSignerFactoryContractInstance = + new SafeWebAuthnSignerFactoryContract_v0_2_1( + chainId, + safeProvider, + safeVersion, + contractAddress, + customContractAbi as SafeWebAuthnSignerFactoryContract_v0_2_1_Abi + ) + + await safeWebAuthnSignerFactoryContractInstance.init() + + return safeWebAuthnSignerFactoryContractInstance + + default: + throw new Error('Invalid Safe version') + } +} + +export async function getSafeWebAuthnSharedSignerContractInstance( + safeVersion: SafeVersion, + safeProvider: SafeProvider, + contractAddress?: string, + customContractAbi?: JsonFragment | JsonFragment[] | undefined +): Promise { + const chainId = await safeProvider.getChainId() + + switch (safeVersion) { + case '1.4.1': + case '1.3.0': + const safeWebAuthnSharedSignerContractInstance = new SafeWebAuthnSharedSignerContract_v0_2_1( + chainId, + safeProvider, + safeVersion, + contractAddress, + customContractAbi as SafeWebAuthnSharedSignerContract_v0_2_1_Abi + ) + + await safeWebAuthnSharedSignerContractInstance.init() + + return safeWebAuthnSharedSignerContractInstance + + default: + throw new Error('Invalid Safe version') + } +} diff --git a/packages/protocol-kit/src/contracts/safeDeploymentContracts.ts b/packages/protocol-kit/src/contracts/safeDeploymentContracts.ts index cff605dca..2cdf4555a 100644 --- a/packages/protocol-kit/src/contracts/safeDeploymentContracts.ts +++ b/packages/protocol-kit/src/contracts/safeDeploymentContracts.ts @@ -7,6 +7,8 @@ import { MultiSendContractImplementationType, SafeContractImplementationType, SafeProxyFactoryContractImplementationType, + SafeWebAuthnSharedSignerContractImplementationType, + SafeWebAuthnSignerFactoryContractImplementationType, SignMessageLibContractImplementationType, SimulateTxAccessorContractImplementationType } from '@safe-global/protocol-kit/types' @@ -175,3 +177,45 @@ export async function getSimulateTxAccessorContract({ } return simulateTxAccessorContract } + +export async function getSafeWebAuthnSignerFactoryContract({ + safeProvider, + safeVersion, + customContracts +}: GetContractInstanceProps): Promise { + const safeWebAuthnSignerFactoryContract = await safeProvider.getSafeWebAuthnSignerFactoryContract( + { + safeVersion, + customContractAddress: customContracts?.safeWebAuthnSignerFactoryAddress, + customContractAbi: customContracts?.safeWebAuthnSignerFactoryAbi + } + ) + + const isContractDeployed = await safeProvider.isContractDeployed( + await safeWebAuthnSignerFactoryContract.getAddress() + ) + if (!isContractDeployed) { + throw new Error('safeWebAuthnSignerFactory contract is not deployed on the current network') + } + return safeWebAuthnSignerFactoryContract +} + +export async function getSafeWebAuthnSharedSignerContract({ + safeProvider, + safeVersion, + customContracts +}: GetContractInstanceProps): Promise { + const safeWebAuthnSharedSignerContract = await safeProvider.getSafeWebAuthnSharedSignerContract({ + safeVersion, + customContractAddress: customContracts?.safeWebAuthnSharedSignerAddress, + customContractAbi: customContracts?.safeWebAuthnSharedSignerAbi + }) + + const isContractDeployed = await safeProvider.isContractDeployed( + await safeWebAuthnSharedSignerContract.getAddress() + ) + if (!isContractDeployed) { + throw new Error('safeWebAuthnSharedSigner contract is not deployed on the current network') + } + return safeWebAuthnSharedSignerContract +} diff --git a/packages/protocol-kit/src/contracts/utils.ts b/packages/protocol-kit/src/contracts/utils.ts index 55bccc402..442ea9f3e 100644 --- a/packages/protocol-kit/src/contracts/utils.ts +++ b/packages/protocol-kit/src/contracts/utils.ts @@ -386,7 +386,8 @@ export function toTxResult( } export function isTypedDataSigner(signer: any): signer is AbstractSigner { - return (signer as unknown as AbstractSigner).signTypedData !== undefined + const isPasskeySigner = !!signer?.passkeyRawId + return (signer as unknown as AbstractSigner).signTypedData !== undefined || !isPasskeySigner } /** diff --git a/packages/protocol-kit/src/index.ts b/packages/protocol-kit/src/index.ts index 53a3dc80d..2c752e68d 100644 --- a/packages/protocol-kit/src/index.ts +++ b/packages/protocol-kit/src/index.ts @@ -16,7 +16,9 @@ import { getMultiSendContract, getProxyFactoryContract, getSafeContract, - getSignMessageLibContract + getSignMessageLibContract, + getSafeWebAuthnSignerFactoryContract, + getSafeWebAuthnSharedSignerContract } from './contracts/safeDeploymentContracts' import { PREDETERMINED_SALT_NONCE, @@ -33,6 +35,9 @@ import { estimateTxGas, estimateSafeTxGas, estimateSafeDeploymentGas, + extractPasskeyData, + getDefaultFCLP256VerifierAddress, + extractPasskeyCoordinates, validateEthereumAddress, validateEip3770Address } from './utils' @@ -62,11 +67,16 @@ import { generateTypedData } from './utils/eip-712' +import PasskeySigner from './utils/passkeys/PasskeySigner' +import getPasskeyOwnerAddress from './utils/passkeys/getPasskeyOwnerAddress' + export { estimateTxBaseGas, estimateTxGas, estimateSafeTxGas, estimateSafeDeploymentGas, + extractPasskeyData, + extractPasskeyCoordinates, ContractManager, CreateCallBaseContract, createERC20TokenTransferTransaction, @@ -91,6 +101,9 @@ export { getProxyFactoryContract, getSafeContract, getSignMessageLibContract, + getSafeWebAuthnSignerFactoryContract, + getSafeWebAuthnSharedSignerContract, + getDefaultFCLP256VerifierAddress, isGasTokenCompatibleWithHandlePayment, predictSafeAddress, getPredictedSafeAddressInitCode, @@ -109,7 +122,9 @@ export { generateTypedData, SafeProvider, EthSafeTransaction, - EthSafeMessage + EthSafeMessage, + PasskeySigner, + getPasskeyOwnerAddress } export * from './types' diff --git a/packages/protocol-kit/src/types/contracts.ts b/packages/protocol-kit/src/types/contracts.ts index 612f3fd28..cc99bd8e2 100644 --- a/packages/protocol-kit/src/types/contracts.ts +++ b/packages/protocol-kit/src/types/contracts.ts @@ -23,6 +23,8 @@ import SimulateTxAccessorContract_v1_3_0 from '@safe-global/protocol-kit/contrac import SimulateTxAccessorContract_v1_4_1 from '@safe-global/protocol-kit/contracts/SimulateTxAccessor/v1.4.1/SimulateTxAccessorContract_v1_4_1' import CreateCallContract_v1_3_0 from '@safe-global/protocol-kit/contracts/CreateCall/v1.3.0/CreateCallContract_v1_3_0' import CreateCallContract_v1_4_1 from '@safe-global/protocol-kit/contracts/CreateCall/v1.4.1/CreateCallContract_v1_4_1' +import SafeWebAuthnSignerFactoryContract_v0_2_1 from '@safe-global/protocol-kit/contracts/SafeWebAuthnSignerFactory/v0.2.1/SafeWebAuthnSignerFactoryContract_v0_2_1' +import SafeWebAuthnSharedSignerContract_v0_2_1 from '@safe-global/protocol-kit/contracts/SafeWebAuthnSharedSigner/v0.2.1/SafeWebAuthnSharedSignerContract_v0_2_1' // Safe contract implementation types export type SafeContractImplementationType = @@ -70,6 +72,13 @@ export type CreateCallContractImplementationType = | CreateCallContract_v1_3_0 | CreateCallContract_v1_4_1 +// SafeWebAuthnSignerFactory contract implementation types +export type SafeWebAuthnSignerFactoryContractImplementationType = + SafeWebAuthnSignerFactoryContract_v0_2_1 + +export type SafeWebAuthnSharedSignerContractImplementationType = + SafeWebAuthnSharedSignerContract_v0_2_1 + export type GetContractProps = { safeVersion: SafeVersion customContractAddress?: string @@ -110,6 +119,14 @@ export type ContractNetworkConfig = { simulateTxAccessorAddress: string /** simulateTxAccessorAbi - Abi of the SimulateTxAccessor contract deployed on a specific network */ simulateTxAccessorAbi?: JsonFragment | JsonFragment[] + /** safeWebAuthnSignerFactoryAddress - Address of the SafeWebAuthnSignerFactory contract deployed on a specific network */ + safeWebAuthnSignerFactoryAddress: string + /** safeWebAuthnSignerFactoryAbi - Abi of the SafeWebAuthnSignerFactory contract deployed on a specific network */ + safeWebAuthnSignerFactoryAbi?: JsonFragment | JsonFragment[] + /** safeWebAuthnSharedSignerAddress - Address of the SafeWebAuthnSharedSigner contract deployed on a specific network */ + safeWebAuthnSharedSignerAddress: string + /** safeWebAuthnSharedSignerAbi - Abi of the SafeWebAuthnSharedSigner contract deployed on a specific network */ + safeWebAuthnSharedSignerAbi?: JsonFragment | JsonFragment[] } export type ContractNetworksConfig = { diff --git a/packages/protocol-kit/src/types/index.ts b/packages/protocol-kit/src/types/index.ts index 6fd85f361..54c7b9fbe 100644 --- a/packages/protocol-kit/src/types/index.ts +++ b/packages/protocol-kit/src/types/index.ts @@ -4,3 +4,4 @@ export * from './safeFactory' export * from './safeProvider' export * from './signing' export * from './transactions' +export * from './passkeys' diff --git a/packages/protocol-kit/src/types/passkeys.ts b/packages/protocol-kit/src/types/passkeys.ts new file mode 100644 index 000000000..912c73e5c --- /dev/null +++ b/packages/protocol-kit/src/types/passkeys.ts @@ -0,0 +1,10 @@ +export type PasskeyCoordinates = { + x: string + y: string +} + +export type PasskeyArgType = { + rawId: string // required to sign data + coordinates: PasskeyCoordinates // required to sign data + customVerifierAddress?: string // optional +} diff --git a/packages/protocol-kit/src/types/safeProvider.ts b/packages/protocol-kit/src/types/safeProvider.ts index af47b84bb..c9a066fc2 100644 --- a/packages/protocol-kit/src/types/safeProvider.ts +++ b/packages/protocol-kit/src/types/safeProvider.ts @@ -1,3 +1,6 @@ +import PasskeySigner from '../utils/passkeys/PasskeySigner' +import { PasskeyArgType } from './passkeys' + export type RequestArguments = { readonly method: string readonly params?: readonly unknown[] | object @@ -11,12 +14,12 @@ export type HexAddress = string export type PrivateKey = string export type HttpTransport = string export type SocketTransport = string -export type SafeSigner = HexAddress | PrivateKey +export type SafeSigner = HexAddress | PrivateKey | PasskeySigner export type SafeProviderConfig = { /** signerOrProvider - Ethers signer or provider */ provider: Eip1193Provider | HttpTransport | SocketTransport - signer?: HexAddress | PrivateKey + signer?: HexAddress | PrivateKey | PasskeySigner | PasskeyArgType } export type SafeProviderTransaction = { diff --git a/packages/protocol-kit/src/types/transactions.ts b/packages/protocol-kit/src/types/transactions.ts index 15582c2fa..b895ccb12 100644 --- a/packages/protocol-kit/src/types/transactions.ts +++ b/packages/protocol-kit/src/types/transactions.ts @@ -5,6 +5,7 @@ import { SafeProviderConfig } from './safeProvider' import { SafeContractImplementationType } from './contracts' import { ContractNetworksConfig } from './contracts' import { PredictedSafeProps } from './safeConfig' +import { PasskeyArgType } from './passkeys' export type CreateTransactionProps = { /** transactions - The transaction array to process */ @@ -53,6 +54,13 @@ export type AddOwnerTxParams = { threshold?: number } +export type AddPasskeyOwnerTxParams = { + /** passkey - The passkey of the new owner */ + passkey: PasskeyArgType + /** threshold - The new threshold */ + threshold?: number +} + export type RemoveOwnerTxParams = { /** ownerAddress - The address of the owner that will be removed */ ownerAddress: string @@ -60,9 +68,35 @@ export type RemoveOwnerTxParams = { threshold?: number } -export type SwapOwnerTxParams = { - /** oldOwnerAddress - The old owner address */ - oldOwnerAddress: string - /** newOwnerAddress - The new owner address */ - newOwnerAddress: string +export type RemovePasskeyOwnerTxParams = { + /** passkey - The passkey of the owner that will be removed */ + passkey: PasskeyArgType + /** threshold - The new threshold */ + threshold?: number } + +export type SwapOwnerTxParams = + | { + /** oldOwnerAddress - The old owner address */ + oldOwnerAddress: string + /** newOwnerAddress - The new owner address */ + newOwnerAddress: string + } + | { + /** oldOwnerPasskey - The old owner passkey */ + oldOwnerPasskey: PasskeyArgType + /** newOwnerAddress - The new owner address */ + newOwnerAddress: string + } + | { + /** oldOwnerAddress - The old owner address */ + oldOwnerAddress: string + /** newOwnerPasskey - The new owner passkey */ + newOwnerPasskey: PasskeyArgType + } + | { + /** oldOwnerPasskey - The old owner passkey */ + oldOwnerPasskey: PasskeyArgType + /** newOwnerPasskey - The new owner passkey */ + newOwnerPasskey: PasskeyArgType + } diff --git a/packages/protocol-kit/src/utils/index.ts b/packages/protocol-kit/src/utils/index.ts index 8d57873a2..1a83548c0 100644 --- a/packages/protocol-kit/src/utils/index.ts +++ b/packages/protocol-kit/src/utils/index.ts @@ -4,3 +4,4 @@ export * from './eip-712' export * from './safeVersions' export * from './signatures' export * from './transactions' +export * from './passkeys' diff --git a/packages/protocol-kit/src/utils/passkeys/PasskeySigner.ts b/packages/protocol-kit/src/utils/passkeys/PasskeySigner.ts new file mode 100644 index 000000000..b6500e0d3 --- /dev/null +++ b/packages/protocol-kit/src/utils/passkeys/PasskeySigner.ts @@ -0,0 +1,266 @@ +import { ethers, AbstractSigner, Provider } from 'ethers' + +import { PasskeyCoordinates, PasskeyArgType } from '../../types/passkeys' +import { + SafeWebAuthnSharedSignerContractImplementationType, + SafeWebAuthnSignerFactoryContractImplementationType +} from '../../types/contracts' +import { getDefaultFCLP256VerifierAddress, hexStringToUint8Array } from './extractPasskeyData' +import isSharedSigner from './isSharedSigner' + +/** + * Represents a Signer that is created using a passkey. + * This class extends the AbstractSigner to implement signer functionalities. + * + * @extends {AbstractSigner} + */ +class PasskeySigner extends AbstractSigner { + /** + * The raw identifier of the passkey. + * see: https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/rawId + */ + passkeyRawId: ArrayBuffer + + /** + * Passkey Coordinates. + */ + coordinates: PasskeyCoordinates + + /** + * P256 Verifier Contract address. + */ + verifierAddress: string + + /** + * chainId + */ + chainId: string + + /** + * signerAddress + */ + signerAddress: string + + /** + * Safe WebAuthn signer factory Contract. + */ + safeWebAuthnSignerFactoryContract: SafeWebAuthnSignerFactoryContractImplementationType + + /** + * Safe WebAuthn shared signer Contract. + */ + safeWebAuthnSharedSignerContract: SafeWebAuthnSharedSignerContractImplementationType + + constructor( + passkey: PasskeyArgType, + safeWebAuthnSignerFactoryContract: SafeWebAuthnSignerFactoryContractImplementationType, + safeWebAuthnSharedSignerContract: SafeWebAuthnSharedSignerContractImplementationType, + provider: Provider, + chainId: string, + signerAddress: string + ) { + super(provider) + + const { rawId, coordinates, customVerifierAddress } = passkey + + this.chainId = chainId + this.passkeyRawId = hexStringToUint8Array(rawId) + this.coordinates = coordinates + this.signerAddress = signerAddress + this.safeWebAuthnSignerFactoryContract = safeWebAuthnSignerFactoryContract + this.safeWebAuthnSharedSignerContract = safeWebAuthnSharedSignerContract + this.verifierAddress = customVerifierAddress || getDefaultFCLP256VerifierAddress(chainId) + } + + static async init( + passkey: PasskeyArgType, + safeWebAuthnSignerFactoryContract: SafeWebAuthnSignerFactoryContractImplementationType, + safeWebAuthnSharedSignerContract: SafeWebAuthnSharedSignerContractImplementationType, + provider: Provider, + safeAddress: string, + owners: string[], + chainId: string + ): Promise { + const { coordinates, customVerifierAddress } = passkey + const verifierAddress = customVerifierAddress || getDefaultFCLP256VerifierAddress(chainId) + + let signerAddress: string + + const isPasskeySharedSigner = await isSharedSigner( + passkey, + safeWebAuthnSharedSignerContract, + safeAddress, + owners, + chainId + ) + + if (isPasskeySharedSigner) { + signerAddress = await safeWebAuthnSharedSignerContract.getAddress() + } else { + ;[signerAddress] = await safeWebAuthnSignerFactoryContract.getSigner([ + BigInt(coordinates.x), + BigInt(coordinates.y), + BigInt(verifierAddress) + ]) + } + + return new PasskeySigner( + passkey, + safeWebAuthnSignerFactoryContract, + safeWebAuthnSharedSignerContract, + provider, + chainId, + signerAddress + ) + } + + /** + * Returns the address associated with the passkey signer. + * @returns {Promise} A promise that resolves to the signer's address. + */ + async getAddress(): Promise { + return this.signerAddress + } + + /** + * Encodes the createSigner contract function. + * @returns {string} The encoded data to create a signer. + */ + encodeCreateSigner(): string { + return this.safeWebAuthnSignerFactoryContract.encode('createSigner', [ + BigInt(this.coordinates.x), + BigInt(this.coordinates.y), + BigInt(this.verifierAddress) + ]) + } + + /** + * Signs the provided data using the passkey. + * @param {Uint8Array} data - The data to be signed. + * @returns {Promise} A promise that resolves to the signed data. + */ + async sign(data: Uint8Array): Promise { + const assertion = (await navigator.credentials.get({ + publicKey: { + challenge: data, + allowCredentials: [{ type: 'public-key', id: this.passkeyRawId }], + userVerification: 'required' + } + })) as PublicKeyCredential & { response: AuthenticatorAssertionResponse } + + if (!assertion?.response?.authenticatorData) { + throw new Error('Failed to sign data with passkey Signer') + } + + const { authenticatorData, signature, clientDataJSON } = assertion.response + + return ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes', 'bytes', 'uint256[2]'], + [ + new Uint8Array(authenticatorData), + extractClientDataFields(clientDataJSON), + extractSignature(signature) + ] + ) + } + + connect(provider: Provider): ethers.Signer { + const passkey: PasskeyArgType = { + rawId: Buffer.from(this.passkeyRawId).toString('hex'), + coordinates: this.coordinates, + customVerifierAddress: this.verifierAddress + } + + return new PasskeySigner( + passkey, + this.safeWebAuthnSignerFactoryContract, + this.safeWebAuthnSharedSignerContract, + provider, + this.chainId, + this.signerAddress + ) + } + + signTransaction(): Promise { + throw new Error('Passkey Signers cannot sign transactions, they can only sign data.') + } + + signMessage(message: string | Uint8Array): Promise { + if (typeof message === 'string') { + return this.sign(ethers.getBytes(message)) + } + + return this.sign(message) + } + + signTypedData(): Promise { + throw new Error('Passkey Signers cannot sign signTypedData, they can only sign data.') + } +} + +export default PasskeySigner + +/** + * Compute the additional client data JSON fields. This is the fields other than `type` and + * `challenge` (including `origin` and any other additional client data fields that may be + * added by the authenticator). + * + * See + * + * @param {ArrayBuffer} clientDataJSON - The client data JSON. + * @returns {string} A hex string of the additional fields from the client data JSON. + * @throws {Error} Throws an error if the client data JSON does not contain the expected 'challenge' field pattern. + */ +function extractClientDataFields(clientDataJSON: ArrayBuffer): string { + const decodedClientDataJSON = new TextDecoder('utf-8').decode(clientDataJSON) + const match = decodedClientDataJSON.match( + /^\{"type":"webauthn.get","challenge":"[A-Za-z0-9\-_]{43}",(.*)\}$/ + ) + + if (!match) { + throw new Error('challenge not found in client data JSON') + } + + const [, fields] = match + return ethers.hexlify(ethers.toUtf8Bytes(fields)) +} + +/** + * Extracts the numeric values r and s from a DER-encoded ECDSA signature. + * This function decodes the signature based on a specific format and validates the encoding at each step. + * + * @param {ArrayBuffer} signature - The DER-encoded signature to be decoded. + * @returns {[bigint, bigint]} A tuple containing two BigInt values, r and s, which are the numeric values extracted from the signature. + * @throws {Error} Throws an error if the signature encoding is invalid or does not meet expected conditions. + */ +function extractSignature(signature: ArrayBuffer): [bigint, bigint] { + const check = (x: boolean) => { + if (!x) { + throw new Error('invalid signature encoding') + } + } + + // Decode the DER signature. Note that we assume that all lengths fit into 8-bit integers, + // which is true for the kinds of signatures we are decoding but generally false. I.e. this + // code should not be used in any serious application. + const view = new DataView(signature) + + // check that the sequence header is valid + check(view.getUint8(0) === 0x30) + check(view.getUint8(1) === view.byteLength - 2) + + // read r and s + const readInt = (offset: number) => { + check(view.getUint8(offset) === 0x02) + const len = view.getUint8(offset + 1) + const start = offset + 2 + const end = start + len + const n = BigInt(ethers.hexlify(new Uint8Array(view.buffer.slice(start, end)))) + check(n < ethers.MaxUint256) + return [n, end] as const + } + const [r, sOffset] = readInt(2) + const [s] = readInt(sOffset) + + return [r, s] +} diff --git a/packages/protocol-kit/src/utils/passkeys/createPasskeyDeploymentTransaction.ts b/packages/protocol-kit/src/utils/passkeys/createPasskeyDeploymentTransaction.ts new file mode 100644 index 000000000..35f07aed8 --- /dev/null +++ b/packages/protocol-kit/src/utils/passkeys/createPasskeyDeploymentTransaction.ts @@ -0,0 +1,50 @@ +import Safe from '../../Safe' +import { PasskeyArgType } from '../../types' +import { EMPTY_DATA } from '../constants' +import SafeProvider from '../../SafeProvider' +import PasskeySigner from './PasskeySigner' + +/** + * Creates the deployment transaction to create a passkey signer. + * + * @param {Safe} safe The protocol-kit instance of the current Safe + * @param {PasskeyArgType} passkey The passkey object + * @returns {Promise<{ to: string; value: string; data: string; }>} The deployment transaction to create a passkey signer. + */ +async function createPasskeyDeploymentTransaction( + safe: Safe, + passkey: PasskeyArgType +): Promise<{ to: string; value: string; data: string }> { + const safeVersion = await safe.getContractVersion() + const safeAddress = await safe.getAddress() + const owners = await safe.getOwners() + + const safePasskeyProvider = await SafeProvider.init( + safe.getSafeProvider().provider, + passkey, + safeVersion, + safe.getContractManager().contractNetworks, + safeAddress, + owners + ) + + const passkeySigner = (await safePasskeyProvider.getExternalSigner()) as PasskeySigner + const passkeyAddress = await passkeySigner!.getAddress() + const provider = safe.getSafeProvider().getExternalProvider() + + const isPasskeyDeployed = (await provider.getCode(passkeyAddress)) !== EMPTY_DATA + + if (isPasskeyDeployed) { + throw new Error('Passkey Signer contract already deployed') + } + + const passkeySignerDeploymentTransaction = { + to: await passkeySigner.safeWebAuthnSignerFactoryContract.getAddress(), + value: '0', + data: passkeySigner.encodeCreateSigner() + } + + return passkeySignerDeploymentTransaction +} + +export default createPasskeyDeploymentTransaction diff --git a/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts b/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts new file mode 100644 index 000000000..04fae878d --- /dev/null +++ b/packages/protocol-kit/src/utils/passkeys/extractPasskeyData.ts @@ -0,0 +1,88 @@ +import { getFCLP256VerifierDeployment } from '@safe-global/safe-modules-deployments' +import { Buffer } from 'buffer' +import { PasskeyCoordinates, PasskeyArgType } from '../../types/passkeys' + +/** + * Extracts and returns the passkey data (coordinates and rawId) from a given passkey Credential. + * + * @param {Credential} passkeyCredential - The passkey credential generated via `navigator.credentials.create()` using correct parameters. + * @returns {Promise} A promise that resolves to an object containing the coordinates and the rawId derived from the passkey. + * @throws {Error} Throws an error if the coordinates could not be extracted + */ +export async function extractPasskeyData(passkeyCredential: Credential): Promise { + const passkey = passkeyCredential as PublicKeyCredential + const attestationResponse = passkey.response as AuthenticatorAttestationResponse + + const publicKey = attestationResponse.getPublicKey() + + if (!publicKey) { + throw new Error('Failed to generate passkey Coordinates. getPublicKey() failed') + } + + const coordinates = await extractPasskeyCoordinates(publicKey) + const rawId = Buffer.from(passkey.rawId).toString('hex') + + return { + rawId, + coordinates + } +} + +/** + * Extracts and returns coordinates from a given passkey public key. + * + * @param {ArrayBuffer} publicKey - The public key of the passkey from which coordinates will be extracted. + * @returns {Promise} A promise that resolves to an object containing the coordinates derived from the public key of the passkey. + * @throws {Error} Throws an error if the coordinates could not be extracted via `crypto.subtle.exportKey()` + */ +export async function extractPasskeyCoordinates( + publicKey: ArrayBuffer +): Promise { + const algorithm = { + name: 'ECDSA', + namedCurve: 'P-256', + hash: { name: 'SHA-256' } + } + + const key = await crypto.subtle.importKey('spki', publicKey, algorithm, true, ['verify']) + + const { x, y } = await crypto.subtle.exportKey('jwk', key) + + const isValidCoordinates = !!x && !!y + + if (!isValidCoordinates) { + throw new Error('Failed to generate passkey Coordinates. crypto.subtle.exportKey() failed') + } + + return { + x: '0x' + Buffer.from(x, 'base64').toString('hex'), + y: '0x' + Buffer.from(y, 'base64').toString('hex') + } +} + +// FIXME use Viem `hexToBytes` +export function hexStringToUint8Array(hexString: string): Uint8Array { + const arr = [] + for (let i = 0; i < hexString.length; i += 2) { + arr.push(parseInt(hexString.substr(i, 2), 16)) + } + return new Uint8Array(arr) +} + +export function getDefaultFCLP256VerifierAddress(chainId: string): string { + const network = BigInt(chainId).toString() + + const FCLP256VerifierDeployment = getFCLP256VerifierDeployment({ + version: '0.2.0', + released: true, + network + }) + + const verifierAddress = FCLP256VerifierDeployment?.networkAddresses[network] + + if (!verifierAddress) { + throw new Error('FCLP256Verifier address not found') + } + + return verifierAddress +} diff --git a/packages/protocol-kit/src/utils/passkeys/getPasskeyOwnerAddress.ts b/packages/protocol-kit/src/utils/passkeys/getPasskeyOwnerAddress.ts new file mode 100644 index 000000000..61bb99015 --- /dev/null +++ b/packages/protocol-kit/src/utils/passkeys/getPasskeyOwnerAddress.ts @@ -0,0 +1,33 @@ +import SafeProvider from '../../SafeProvider' +import Safe from '../../Safe' +import { PasskeyArgType } from '../../types' + +/** + * Returns the owner address associated with the specific passkey. + * + * @param {Safe} safe The protocol-kit instance of the current Safe + * @param {PasskeyArgType} passkey The passkey to check the owner address + * @returns {Promise} Returns the passkey owner address associated with the passkey + */ +async function getPasskeyOwnerAddress(safe: Safe, passkey: PasskeyArgType): Promise { + const safeVersion = await safe.getContractVersion() + const safeAddress = await safe.getAddress() + const owners = await safe.getOwners() + + const safePasskeyProvider = await SafeProvider.init( + safe.getSafeProvider().provider, + passkey, + safeVersion, + safe.getContractManager().contractNetworks, + safeAddress, + owners + ) + + const passkeySigner = await safePasskeyProvider.getExternalSigner() + + const passkeyOwnerAddress = await passkeySigner!.getAddress() + + return passkeyOwnerAddress +} + +export default getPasskeyOwnerAddress diff --git a/packages/protocol-kit/src/utils/passkeys/index.ts b/packages/protocol-kit/src/utils/passkeys/index.ts new file mode 100644 index 000000000..c05ba8772 --- /dev/null +++ b/packages/protocol-kit/src/utils/passkeys/index.ts @@ -0,0 +1,3 @@ +export * from './extractPasskeyData' +export * from './PasskeySigner' +export * from './getPasskeyOwnerAddress' diff --git a/packages/protocol-kit/src/utils/passkeys/isSharedSigner.ts b/packages/protocol-kit/src/utils/passkeys/isSharedSigner.ts new file mode 100644 index 000000000..579ad37d6 --- /dev/null +++ b/packages/protocol-kit/src/utils/passkeys/isSharedSigner.ts @@ -0,0 +1,40 @@ +import { PasskeyArgType } from '../../types/passkeys' +import { getDefaultFCLP256VerifierAddress } from './extractPasskeyData' +import { SafeWebAuthnSharedSignerContractImplementationType } from '../../types/contracts' + +/** + * Returns true if the passkey signer is a shared signer + * @returns {Promise} A promise that resolves to the signer's address. + */ +async function isSharedSigner( + passkey: PasskeyArgType, + safeWebAuthnSharedSignerContract: SafeWebAuthnSharedSignerContractImplementationType, + safeAddress: string, + owners: string[], + chainId: string +): Promise { + const sharedSignerContractAddress = await safeWebAuthnSharedSignerContract.getAddress() + + // is a shared signer if the shared signer contract address is present in the owners and its configured in the Safe slot + if (safeAddress && owners.includes(sharedSignerContractAddress)) { + const [sharedSignerSlot] = await safeWebAuthnSharedSignerContract.getConfiguration([ + safeAddress + ]) + + const { x, y, verifiers } = sharedSignerSlot + + const passkeyVerifierAddress = + passkey.customVerifierAddress || getDefaultFCLP256VerifierAddress(chainId.toString()) + + const isSharedSigner = + BigInt(passkey.coordinates.x) === x && + BigInt(passkey.coordinates.y) === y && + BigInt(passkeyVerifierAddress) === verifiers + + return isSharedSigner + } + + return false +} + +export default isSharedSigner diff --git a/packages/protocol-kit/src/utils/safeVersions.ts b/packages/protocol-kit/src/utils/safeVersions.ts index 23142699a..28fa89292 100644 --- a/packages/protocol-kit/src/utils/safeVersions.ts +++ b/packages/protocol-kit/src/utils/safeVersions.ts @@ -14,6 +14,7 @@ export enum SAFE_FEATURES { ACCOUNT_ABSTRACTION = 'ACCOUNT_ABSTRACTION', REQUIRED_TXGAS = 'REQUIRED_TXGAS', SIMULATE_AND_REVERT = 'SIMULATE_AND_REVERT', + PASSKEY_SIGNER = 'PASSKEY_SIGNER', SAFE_L2_CONTRACTS = 'SAFE_L2_CONTRACTS' } @@ -25,6 +26,7 @@ const SAFE_FEATURES_BY_VERSION: Record = { [SAFE_FEATURES.ACCOUNT_ABSTRACTION]: '>=1.3.0', [SAFE_FEATURES.REQUIRED_TXGAS]: '<=1.2.0', [SAFE_FEATURES.SIMULATE_AND_REVERT]: '>=1.3.0', + [SAFE_FEATURES.PASSKEY_SIGNER]: '>=1.3.0', [SAFE_FEATURES.SAFE_L2_CONTRACTS]: '>=1.3.0' } diff --git a/packages/protocol-kit/src/utils/signatures/utils.ts b/packages/protocol-kit/src/utils/signatures/utils.ts index dcc26c477..179a606c1 100644 --- a/packages/protocol-kit/src/utils/signatures/utils.ts +++ b/packages/protocol-kit/src/utils/signatures/utils.ts @@ -110,6 +110,7 @@ export async function generateSignature( hash: string ): Promise { const signerAddress = await safeProvider.getSignerAddress() + if (!signerAddress) { throw new Error('SafeProvider must be initialized with a signer to use this method') } diff --git a/packages/protocol-kit/src/utils/transactions/gas.ts b/packages/protocol-kit/src/utils/transactions/gas.ts index dff83c07b..389e19a32 100644 --- a/packages/protocol-kit/src/utils/transactions/gas.ts +++ b/packages/protocol-kit/src/utils/transactions/gas.ts @@ -225,8 +225,8 @@ export async function estimateTxBaseGas( customContracts }) - //@ts-expect-error: Type too complex to represent. //TODO: We should explore contract versions and map to the correct types + //@ts-expect-error: Type too complex to represent. const execTransactionData = safeSingletonContract.encode('execTransaction', [ to, BigInt(value), diff --git a/packages/protocol-kit/src/utils/transactions/utils.ts b/packages/protocol-kit/src/utils/transactions/utils.ts index 583752527..c5de8f6f3 100644 --- a/packages/protocol-kit/src/utils/transactions/utils.ts +++ b/packages/protocol-kit/src/utils/transactions/utils.ts @@ -1,7 +1,15 @@ import { ethers, Interface, getBytes, solidityPacked as solidityPack } from 'ethers' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' import { DEFAULT_SAFE_VERSION } from '@safe-global/protocol-kit/contracts/config' -import { StandardizeSafeTransactionDataProps } from '@safe-global/protocol-kit/types' +import { + AddOwnerTxParams, + AddPasskeyOwnerTxParams, + PasskeyArgType, + RemoveOwnerTxParams, + RemovePasskeyOwnerTxParams, + StandardizeSafeTransactionDataProps, + SwapOwnerTxParams +} from '@safe-global/protocol-kit/types' import { hasSafeFeature, SAFE_FEATURES } from '@safe-global/protocol-kit/utils' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/utils/constants' import { @@ -157,3 +165,27 @@ export function isSafeMultisigTransactionResponse( ): safeTransaction is SafeMultisigTransactionResponse { return (safeTransaction as SafeMultisigTransactionResponse).isExecuted !== undefined } + +type PasskeyParam = { passkey: PasskeyArgType } + +export function isPasskeyParam( + params: + | AddOwnerTxParams + | AddPasskeyOwnerTxParams + | RemoveOwnerTxParams + | RemovePasskeyOwnerTxParams +): params is PasskeyParam { + return (params as PasskeyParam).passkey !== undefined +} + +export function isOldOwnerPasskey( + params: SwapOwnerTxParams +): params is SwapOwnerTxParams & { oldOwnerPasskey: PasskeyArgType } { + return (params as { oldOwnerPasskey: PasskeyArgType }).oldOwnerPasskey !== undefined +} + +export function isNewOwnerPasskey( + params: SwapOwnerTxParams +): params is SwapOwnerTxParams & { newOwnerPasskey: PasskeyArgType } { + return (params as { newOwnerPasskey: PasskeyArgType }).newOwnerPasskey !== undefined +} diff --git a/packages/protocol-kit/tests/e2e/createSafeDeploymentTransaction.test.ts b/packages/protocol-kit/tests/e2e/createSafeDeploymentTransaction.test.ts index 3ccba4d55..254f87e24 100644 --- a/packages/protocol-kit/tests/e2e/createSafeDeploymentTransaction.test.ts +++ b/packages/protocol-kit/tests/e2e/createSafeDeploymentTransaction.test.ts @@ -60,7 +60,7 @@ describe('createSafeDeploymentTransaction', () => { to: safeFactoryAddress, value: '0', // safe deployment data (createProxyWithNonce) - data: '0x1688f0b900000000000000000000000031233647996a4e0d623c9ba42ce8538c2531e22b0000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000003f20fb66d809929e59d9ab1e725d307d696b5593000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + data: '0x1688f0b9000000000000000000000000880ed85cacd4d9209452170fc4f16d4fddf00c660000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000060457d01a15434df4c05b29aefbb2d94dc8ddbab000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }) }) @@ -81,7 +81,7 @@ describe('createSafeDeploymentTransaction', () => { to: safeFactoryAddress, value: '0', // safe deployment data (createProxyWithNonce) - data: '0x1688f0b90000000000000000000000008e6332da7ccd5430bfb27df39fbf386b463c31a50000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000085692cd6f0b50e6d48b98153cba504a09564e776000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + data: '0x1688f0b900000000000000000000000012d800ba6577c89ea9bd6728ea6ca40bab7114940000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000008bec390d0b38e898788fa2aa4e50c263c48f84e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }) }) @@ -102,7 +102,7 @@ describe('createSafeDeploymentTransaction', () => { to: safeFactoryAddress, value: '0', // safe deployment data (createProxyWithNonce) - data: '0x1688f0b90000000000000000000000001634c531e43c1fd383741a8da6215e4ae08233660000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000085692cd6f0b50e6d48b98153cba504a09564e776000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + data: '0x1688f0b90000000000000000000000001634c531e43c1fd383741a8da6215e4ae08233660000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000008bec390d0b38e898788fa2aa4e50c263c48f84e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }) }) @@ -123,7 +123,7 @@ describe('createSafeDeploymentTransaction', () => { to: safeFactoryAddress, value: '0', // safe deployment data (createProxyWithNonce) - data: '0x1688f0b9000000000000000000000000d7b2104dc288b0abef09086bac0b6ec43dd435340000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000085692cd6f0b50e6d48b98153cba504a09564e776000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + data: '0x1688f0b9000000000000000000000000d7b2104dc288b0abef09086bac0b6ec43dd435340000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000008bec390d0b38e898788fa2aa4e50c263c48f84e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }) }) diff --git a/packages/protocol-kit/tests/e2e/createTransactionBatch.test.ts b/packages/protocol-kit/tests/e2e/createTransactionBatch.test.ts index 0e4547b68..bf8e848d6 100644 --- a/packages/protocol-kit/tests/e2e/createTransactionBatch.test.ts +++ b/packages/protocol-kit/tests/e2e/createTransactionBatch.test.ts @@ -4,10 +4,11 @@ import { deployments } from 'hardhat' import { safeVersionDeployed } from '@safe-global/protocol-kit/hardhat/deploy/deploy-contracts' import Safe, { PredictedSafeProps } from '@safe-global/protocol-kit/index' import { getContractNetworks } from './utils/setupContractNetworks' -import { getERC20Mintable, getSafeWithOwners, getMultiSendCallOnly } from './utils/setupContracts' +import { getSafeWithOwners, getMultiSendCallOnly } from './utils/setupContracts' import { getEip1193Provider } from './utils/setupProvider' import { getAccounts } from './utils/setupTestNetwork' import { OperationType } from '@safe-global/safe-core-sdk-types' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/utils/constants' chai.use(chaiAsPromised) @@ -31,7 +32,6 @@ describe('createTransactionBatch', () => { } return { - erc20Mintable: await getERC20Mintable(), accounts, contractNetworks, predictedSafe, @@ -40,8 +40,8 @@ describe('createTransactionBatch', () => { }) it('should return a batch of the provided transactions', async () => { - const { accounts, contractNetworks, erc20Mintable } = await setupTests() - const [account1, account2] = accounts + const { accounts, contractNetworks } = await setupTests() + const [account1] = accounts const safe = await getSafeWithOwners([account1.address]) const provider = getEip1193Provider() @@ -54,12 +54,9 @@ describe('createTransactionBatch', () => { }) const dumpTransfer = { - to: await erc20Mintable.getAddress(), - value: '0', - data: erc20Mintable.interface.encodeFunctionData('transfer', [ - account2.address, - AMOUNT_TO_TRANSFER - ]), + to: ZERO_ADDRESS, + value: AMOUNT_TO_TRANSFER, + data: '0x', operation: OperationType.Call } @@ -72,7 +69,7 @@ describe('createTransactionBatch', () => { chai.expect(batchTransaction).to.be.deep.equal({ to: multiSendContractAddress, value: '0', - data: '0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001320067b5656d60a809915323bf2c40a8bef15a152e3e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000ffcf8fdee72ac11b5c542428b35eef5769c409f000000000000000000000000000000000000000000000000006f05b59d3b200000067b5656d60a809915323bf2c40a8bef15a152e3e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000ffcf8fdee72ac11b5c542428b35eef5769c409f000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000' + data: '0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000aa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f05b59d3b20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f05b59d3b20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }) }) }) diff --git a/packages/protocol-kit/tests/e2e/passkey.test.ts b/packages/protocol-kit/tests/e2e/passkey.test.ts new file mode 100644 index 000000000..0b4ed8162 --- /dev/null +++ b/packages/protocol-kit/tests/e2e/passkey.test.ts @@ -0,0 +1,1038 @@ +import { safeVersionDeployed } from '@safe-global/protocol-kit/hardhat/deploy/deploy-contracts' +import { OperationType } from '@safe-global/safe-core-sdk-types' +import Safe, { + getPasskeyOwnerAddress, + PasskeySigner, + PredictedSafeProps, + SafeProvider +} from '@safe-global/protocol-kit' +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import sinon from 'sinon' +import sinonChai from 'sinon-chai' +import { deployments } from 'hardhat' +import crypto from 'crypto' +import { + getSafeWebAuthnSharedSignerContract, + getSafeWebAuthnSignerFactoryContract +} from '@safe-global/protocol-kit/contracts/safeDeploymentContracts' +import { getContractNetworks } from './utils/setupContractNetworks' +import { getSafeWithOwners, getWebAuthnContract } from './utils/setupContracts' +import { getEip1193Provider } from './utils/setupProvider' +import { waitSafeTxReceipt } from './utils/transactions' +import { getAccounts } from './utils/setupTestNetwork' +import { itif, describeif } from './utils/helpers' +import { createMockPasskey, getWebAuthnCredentials, deployPasskeysContract } from './utils/passkeys' + +chai.use(chaiAsPromised) +chai.use(sinonChai) + +const webAuthnCredentials = getWebAuthnCredentials() + +if (!global.crypto) { + global.crypto = crypto as unknown as Crypto +} + +Object.defineProperty(global, 'navigator', { + value: { + credentials: { + create: sinon.stub().callsFake(webAuthnCredentials.create.bind(webAuthnCredentials)), + get: sinon.stub().callsFake(webAuthnCredentials.get.bind(webAuthnCredentials)) + } + }, + writable: true +}) + +describe('Passkey', () => { + const setupTests = deployments.createFixture(async ({ deployments, getChainId }) => { + await deployments.fixture() + + const webAuthnContract = await getWebAuthnContract() + const customVerifierAddress = await webAuthnContract.getAddress() + + const passkey1 = { ...(await createMockPasskey('chucknorris')), customVerifierAddress } + const passkey2 = { ...(await createMockPasskey('brucelee')), customVerifierAddress } + + const chainId = BigInt(await getChainId()) + const contractNetworks = await getContractNetworks(chainId) + const provider = getEip1193Provider() + const safeProvider = new SafeProvider({ provider }) + const customContracts = contractNetworks?.[chainId.toString()] + + const safeWebAuthnSignerFactoryContract = await getSafeWebAuthnSignerFactoryContract({ + safeProvider, + safeVersion: '1.4.1', + customContracts + }) + + const safeWebAuthnSharedSignerContract = await getSafeWebAuthnSharedSignerContract({ + safeProvider, + safeVersion: '1.4.1', + customContracts + }) + + const passkeySigner1 = await PasskeySigner.init( + passkey1, + safeWebAuthnSignerFactoryContract, + safeWebAuthnSharedSignerContract, + provider, + '', + [], + chainId + ) + + const passkeySigner2 = await PasskeySigner.init( + passkey2, + safeWebAuthnSignerFactoryContract, + safeWebAuthnSharedSignerContract, + provider, + '', + [], + chainId + ) + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig: { + owners: [await passkeySigner1.getAddress()], + threshold: 1 + }, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + return { + accounts: await getAccounts(), + contractNetworks, + predictedSafe, + provider, + passkeys: [passkey1, passkey2], + passkeySigners: [passkeySigner1, passkeySigner2], + safeWebAuthnSharedSignerContract, + safeWebAuthnSignerFactoryContract + } + }) + + describe('isOwner', async () => { + describe('getPasskeyOwnerAddress', async () => { + itif(safeVersionDeployed < '1.3.0')( + 'should fail for Safe versions lower than 1.3.0', + async () => { + const { + contractNetworks, + provider, + accounts: [account1], + passkeys: [passkey1] + } = await setupTests() + const safe = await getSafeWithOwners([account1.address]) + const safeAddress = await safe.getAddress() + + // Create a Safe instance with an EOA signer + const safeSdk = await Safe.init({ + provider, + safeAddress, + contractNetworks + }) + + chai + .expect(getPasskeyOwnerAddress(safeSdk, passkey1)) + .to.be.rejectedWith( + 'Current version of the Safe does not support the Passkey signer functionality' + ) + } + ) + + itif(safeVersionDeployed >= '1.3.0')( + 'should return the address of the passkey signer', + async () => { + const { + contractNetworks, + provider, + passkeys: [passkey1], + passkeySigners: [passkeySigner1], + safeWebAuthnSignerFactoryContract + } = await setupTests() + const passkeySigner1Address = await passkeySigner1.getAddress() + const safe = await getSafeWithOwners([passkeySigner1Address]) + + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks, + signer: passkey1 + }) + + const passkeyAddress = await getPasskeyOwnerAddress(safeSdk, passkey1) + const [expectedPasskeyAddress] = await safeWebAuthnSignerFactoryContract.getSigner([ + BigInt(passkey1.coordinates.x), + BigInt(passkey1.coordinates.y), + BigInt(passkey1.customVerifierAddress) + ]) + + chai.expect(passkeyAddress).to.equals(expectedPasskeyAddress) + } + ) + + itif(safeVersionDeployed >= '1.3.0')( + 'should return the shared signer address of the passkey signer', + async () => { + const { + accounts: [EOAaccount1], + contractNetworks, + provider, + passkeys: [passkey1], + safeWebAuthnSharedSignerContract + } = await setupTests() + + const sharedSignerContractAddress = await safeWebAuthnSharedSignerContract.getAddress() + + const safe = await getSafeWithOwners([EOAaccount1.address]) + + // configure the shared Signer passkey in the Safe Slot + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + + const passkeyOwnerConfiguration = { + ...passkey1.coordinates, + verifiers: passkey1.customVerifierAddress + } + + const { data: addSharedSignerAddressOwner } = await safeSdk.createAddOwnerTx({ + ownerAddress: sharedSignerContractAddress + }) + + const configureSharedSignerTransaction = { + to: sharedSignerContractAddress, + value: '0', + data: safeWebAuthnSharedSignerContract.encode('configure', [passkeyOwnerConfiguration]), + operation: OperationType.DelegateCall // DelegateCall required into the SafeWebAuthnSharedSigner instance in order for it to set its configuration. + } + + const transactions = [addSharedSignerAddressOwner, configureSharedSignerTransaction] + + const configureSharedSignerSafeTransaction = await safeSdk.createTransaction({ + transactions + }) + + // Sign the configure the shared Signer transaction with the EOA signer + const signedConfigureSharedSignerSafeTransaction = await safeSdk.signTransaction( + configureSharedSignerSafeTransaction + ) + + chai.expect(await safeSdk.isOwner(await getPasskeyOwnerAddress(safeSdk, passkey1))).to.be + .false + + await safeSdk.executeTransaction(signedConfigureSharedSignerSafeTransaction) + + const passkeyAddress = await getPasskeyOwnerAddress(safeSdk, passkey1) + + chai.expect(passkeyAddress).to.equals(sharedSignerContractAddress) + } + ) + }) + + itif(safeVersionDeployed >= '1.3.0')('should fail if the Safe is not deployed', async () => { + const { + predictedSafe, + contractNetworks, + provider, + passkeys: [passkey1] + } = await setupTests() + const safeSdk = await Safe.init({ + provider, + predictedSafe, + contractNetworks, + signer: passkey1 + }) + + const passkeyAddress = await getPasskeyOwnerAddress(safeSdk, passkey1) + + chai.expect(safeSdk.isOwner(passkeyAddress)).to.be.rejectedWith('Safe is not deployed') + }) + + itif(safeVersionDeployed >= '1.3.0')( + 'should return true if passkey signer is an owner of the connected Safe', + async () => { + const { + contractNetworks, + provider, + passkeys: [passkey1], + passkeySigners: [passkeySigner1] + } = await setupTests() + const passkeySigner1Address = await passkeySigner1.getAddress() + const safe = await getSafeWithOwners([passkeySigner1Address]) + + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks, + signer: passkey1 + }) + + const passkeyAddress = await getPasskeyOwnerAddress(safeSdk, passkey1) + + const isOwner = await safeSdk.isOwner(passkeyAddress) + chai.expect(isOwner).to.be.true + } + ) + + itif(safeVersionDeployed >= '1.3.0')( + 'should return false if an account is not an owner of the connected Safe', + async () => { + const { + contractNetworks, + provider, + passkeys: [passkey1, passkey2], + passkeySigners: [passkeySigner1] + } = await setupTests() + const passkeySigner1Address = await passkeySigner1.getAddress() + const safe = await getSafeWithOwners([passkeySigner1Address]) + + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks, + signer: passkey1 + }) + + const passkeyAddress = await getPasskeyOwnerAddress(safeSdk, passkey2) + + const isOwner = await safeSdk.isOwner(passkeyAddress) + chai.expect(isOwner).to.be.false + } + ) + + describe('Shared Signer passkey', async () => { + itif(safeVersionDeployed >= '1.3.0')( + 'should return true if the passkey is a Shared Signer passkey owner of the connected Safe', + async () => { + const { + accounts: [EOAaccount1], + contractNetworks, + provider, + passkeys: [passkey1], + safeWebAuthnSharedSignerContract + } = await setupTests() + + const sharedSignerContractAddress = await safeWebAuthnSharedSignerContract.getAddress() + + const safe = await getSafeWithOwners([EOAaccount1.address]) + + // configure the shared Signer passkey in the Safe Slot + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + + const passkeyOwnerConfiguration = { + ...passkey1.coordinates, + verifiers: passkey1.customVerifierAddress + } + + const { data: addSharedSignerAddressOwner } = await safeSdk.createAddOwnerTx({ + ownerAddress: sharedSignerContractAddress + }) + + const configureSharedSignerTransaction = { + to: sharedSignerContractAddress, + value: '0', + data: safeWebAuthnSharedSignerContract.encode('configure', [passkeyOwnerConfiguration]), + operation: OperationType.DelegateCall // DelegateCall required into the SafeWebAuthnSharedSigner instance in order for it to set its configuration. + } + + const transactions = [addSharedSignerAddressOwner, configureSharedSignerTransaction] + + const configureSharedSignerSafeTransaction = await safeSdk.createTransaction({ + transactions + }) + + // Sign the configure the shared Signer transaction with the EOA signer + const signedConfigureSharedSignerSafeTransaction = await safeSdk.signTransaction( + configureSharedSignerSafeTransaction + ) + + chai.expect(await safeSdk.isOwner(await getPasskeyOwnerAddress(safeSdk, passkey1))).to.be + .false + + await safeSdk.executeTransaction(signedConfigureSharedSignerSafeTransaction) + + chai.expect(await safeSdk.isOwner(await getPasskeyOwnerAddress(safeSdk, passkey1))).to.be + .true + } + ) + + describeif(safeVersionDeployed >= '1.3.0')('swapOwner', () => { + it('should rotate a shared signer passkey owner', async () => { + const { + accounts: [EOAaccount1], + contractNetworks, + provider, + passkeys: [sharedSignerPasskey1, passkey2], + safeWebAuthnSharedSignerContract + // passkeySigners: [sharedPasskeySigner1] + } = await setupTests() + + const sharedSignerContractAddress = await safeWebAuthnSharedSignerContract.getAddress() + + const safe = await getSafeWithOwners([EOAaccount1.address]) + const safeAddress = await safe.getAddress() + + // configure the shared Signer passkey in the Safe Slot + const safeSdk = await Safe.init({ + provider, + safeAddress, + contractNetworks + }) + + const passkeyOwnerConfiguration = { + ...sharedSignerPasskey1.coordinates, + verifiers: sharedSignerPasskey1.customVerifierAddress + } + + const { data: addSharedSignerAddressOwner } = await safeSdk.createAddOwnerTx({ + ownerAddress: sharedSignerContractAddress + }) + + const configureSharedSignerTransaction = { + to: sharedSignerContractAddress, + value: '0', + data: safeWebAuthnSharedSignerContract.encode('configure', [passkeyOwnerConfiguration]), + operation: OperationType.DelegateCall // DelegateCall required into the SafeWebAuthnSharedSigner instance in order for it to set its configuration. + } + + const transactions = [addSharedSignerAddressOwner, configureSharedSignerTransaction] + + const configureSharedSignerSafeTransaction = await safeSdk.createTransaction({ + transactions + }) + + // Sign the configure the shared Signer transaction with the EOA signer + const signedConfigureSharedSignerSafeTransaction = await safeSdk.signTransaction( + configureSharedSignerSafeTransaction + ) + + await safeSdk.executeTransaction(signedConfigureSharedSignerSafeTransaction) + + chai.expect( + await safeSdk.isOwner(await getPasskeyOwnerAddress(safeSdk, sharedSignerPasskey1)) + ).to.be.true + chai.expect(await safeSdk.isOwner(await getPasskeyOwnerAddress(safeSdk, passkey2))).to.be + .false + + const sharedSignerSafeSdk = await Safe.init({ + provider, + safeAddress, + contractNetworks, + signer: sharedSignerPasskey1 + }) + + // rotate the shared signer passkey + const swapOwnerTx = await sharedSignerSafeSdk.createSwapOwnerTx({ + oldOwnerPasskey: sharedSignerPasskey1, + newOwnerPasskey: passkey2 + }) + + const signerSwapOwnerTx = await sharedSignerSafeSdk.signTransaction(swapOwnerTx) + + await safeSdk.executeTransaction(signerSwapOwnerTx) + + chai.expect( + await safeSdk.isOwner(await getPasskeyOwnerAddress(safeSdk, sharedSignerPasskey1)) + ).to.be.false + chai.expect(await safeSdk.isOwner(await getPasskeyOwnerAddress(safeSdk, passkey2))).to.be + .true + }) + }) + }) + }) + describeif(safeVersionDeployed >= '1.3.0')('signTransaction', async () => { + it('should sign a transaction with the current passkey signer', async () => { + const { + accounts: [account1], + contractNetworks, + provider, + passkeys: [passkey1], + passkeySigners: [passkeySigner1] + } = await setupTests() + + const passkeySigner1Address = await passkeySigner1.getAddress() + const safe = await getSafeWithOwners([passkeySigner1Address]) + const safeAddress = await safe.getAddress() + + // First create transaction for the deployment of the passkey signer + await deployPasskeysContract([passkeySigner1], account1.signer) + + // Passkey signer should be deployed now + chai.expect(await account1.signer.provider.getCode(passkeySigner1Address)).length.to.be.gt(2) + + const safeSdk = await Safe.init({ + provider, + safeAddress, + contractNetworks, + signer: passkey1 + }) + const tx = await safeSdk.createTransaction({ + transactions: [{ to: safeAddress, value: '0', data: '0x' }] + }) + + chai.expect(tx.signatures.size).to.be.eq(0) + const signedTx = await safeSdk.signTransaction(tx) + chai.expect(tx.signatures.size).to.be.eq(0) + chai.expect(signedTx.signatures.size).to.be.eq(1) + // Create a Safe instance with an EOA signer to execute the transaction + const safeSdkEOA = await Safe.init({ + provider, + safeAddress, + contractNetworks + }) + + // The transaction can only be executed by an EOA signer + const txResponse = await safeSdkEOA.executeTransaction(signedTx) + await waitSafeTxReceipt(txResponse) + }) + + it('should fail if the signature is added by an account that is not an owner', async () => { + const { + contractNetworks, + provider, + passkeys, + passkeySigners: [passkeySigner1] + } = await setupTests() + const passkey2 = passkeys[1] + const passkeySigner1Address = await passkeySigner1.getAddress() + const safe = await getSafeWithOwners([passkeySigner1Address]) + const safeAddress = await safe.getAddress() + + const safeSdk = await Safe.init({ + provider, + safeAddress, + contractNetworks, + signer: passkey2 + }) + + const tx = await safeSdk.createTransaction({ + transactions: [{ to: safeAddress, value: '0', data: '0x' }] + }) + + await chai + .expect(safeSdk.signTransaction(tx)) + .to.be.rejectedWith('Transactions can only be signed by Safe owners') + }) + }) + + describeif(safeVersionDeployed >= '1.3.0')('createRemoveOwnerTx', () => { + it('should remove a passkey owner of a Safe and automaticaly decrement the threshold', async () => { + const { + accounts: [eoaOwner1, eoaOwner2], + contractNetworks, + provider, + passkeys: [passkeyFormerOwner], + passkeySigners: [passkeySigner] + } = await setupTests() + + const passkeyFormerOwnerAddress = await passkeySigner.getAddress() + const safe = await getSafeWithOwners( + [eoaOwner1.address, eoaOwner2.address, passkeyFormerOwnerAddress], + 2 + ) + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + + chai.expect(await safeSdk.getThreshold()).to.be.eq(2) + + chai.expect(await safeSdk.getOwners()).to.include.members([passkeyFormerOwnerAddress]) + + const removeOwnerTx = await safeSdk.createRemoveOwnerTx({ + passkey: passkeyFormerOwner + }) + + const approverSdk = await safeSdk.connect({ + signer: eoaOwner2.address + }) + + const approvedTx = await approverSdk.signTransaction(removeOwnerTx) + const result = await safeSdk.executeTransaction(approvedTx) + await waitSafeTxReceipt(result) + + chai.expect(await safeSdk.getOwners()).to.not.include(passkeyFormerOwner) + chai.expect(await safeSdk.getThreshold()).to.be.eq(1) + }) + + it('should remove a passkey owner of a Safe and set the threshold', async () => { + const { + accounts: [eoaOwner1, eoaOwner2], + contractNetworks, + provider, + passkeys: [passkeyFormerOwner], + passkeySigners: [passkeySigner] + } = await setupTests() + + const passkeyFormerOwnerAddress = await passkeySigner.getAddress() + const safe = await getSafeWithOwners( + [eoaOwner1.address, eoaOwner2.address, passkeyFormerOwnerAddress], + 2 + ) + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + + chai.expect(await safeSdk.getThreshold()).to.be.eq(2) + + chai.expect(await safeSdk.getOwners()).to.include.members([passkeyFormerOwnerAddress]) + + const removeOwnerTx = await safeSdk.createRemoveOwnerTx({ + passkey: passkeyFormerOwner, + threshold: 2 + }) + + const approverSdk = await safeSdk.connect({ + signer: eoaOwner2.address + }) + + const approvedTx = await approverSdk.signTransaction(removeOwnerTx) + const result = await safeSdk.executeTransaction(approvedTx) + await waitSafeTxReceipt(result) + + chai.expect(await safeSdk.getOwners()).to.not.include(passkeyFormerOwner) + chai.expect(await safeSdk.getThreshold()).to.be.eq(2) + }) + + it('should prevent a former passkey owner of a Safe to sign transactions', async () => { + const { + accounts: [account], + contractNetworks, + provider, + passkeys: [passkeyFormerOwner], + passkeySigners: [passkeySigner] + } = await setupTests() + + const passkeyFormerOwnerAddress = await passkeySigner.getAddress() + const safe = await getSafeWithOwners([account.address, passkeyFormerOwnerAddress], 1) + + const safeAddress = await safe.getAddress() + const safeSdk = await Safe.init({ + provider, + safeAddress, + contractNetworks + }) + + await deployPasskeysContract([passkeySigner], account.signer) + + const signerSdk = await safeSdk.connect({ + signer: passkeyFormerOwner + }) + + chai.expect(await safeSdk.getOwners()).to.include.members([passkeyFormerOwnerAddress]) + + const removeOwnerTx = await safeSdk.createRemoveOwnerTx({ + passkey: passkeyFormerOwner, + threshold: 1 + }) + + const result = await safeSdk.executeTransaction(removeOwnerTx) + await waitSafeTxReceipt(result) + + chai.expect(await safeSdk.getOwners()).to.not.include(passkeyFormerOwner) + + const safeTransactionData = { + to: safeAddress, + value: '0', + data: '0x' + } + + const tx = await safeSdk.createTransaction({ transactions: [safeTransactionData] }) + chai + .expect(signerSdk.signTransaction(tx)) + .to.be.rejectedWith('Transactions can only be signed by Safe owners') + }) + }) + + describeif(safeVersionDeployed >= '1.3.0')('createSwapOwnerTx', () => { + it('should replace any owner of a Safe with a passkey', async () => { + const { + accounts: [eoaOwner1, eoaOwner2, eoaOwner3], + contractNetworks, + provider, + passkeys: [passkeyNewOwner], + passkeySigners: [passkeySigner] + } = await setupTests() + + const passkeyNewOwnerAddress = await passkeySigner.getAddress() + const safe = await getSafeWithOwners( + [eoaOwner1.address, eoaOwner2.address, eoaOwner3.address], + 2 + ) + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + + const currentOwners = await safeSdk.getOwners() + + chai + .expect(currentOwners) + .to.include.members([eoaOwner1.address, eoaOwner2.address, eoaOwner3.address]) + chai.expect(currentOwners).to.not.include(passkeyNewOwnerAddress) + + chai.expect(await safeSdk.getSafeProvider().isContractDeployed(passkeyNewOwnerAddress)).to.be + .false + + const formerOwner = eoaOwner3.address + const swapOwnerTx = await safeSdk.createSwapOwnerTx({ + oldOwnerAddress: formerOwner, + newOwnerPasskey: passkeyNewOwner + }) + + const approverSdk = await safeSdk.connect({ + signer: eoaOwner2.address + }) + + const approvedTx = await approverSdk.signTransaction(swapOwnerTx) + const result = await safeSdk.executeTransaction(approvedTx) + await waitSafeTxReceipt(result) + + const newOwners = await safeSdk.getOwners() + + chai + .expect(newOwners) + .to.include.members([eoaOwner1.address, eoaOwner2.address, passkeyNewOwnerAddress]) + chai.expect(newOwners).to.not.include(formerOwner) + + chai.expect(await safeSdk.getSafeProvider().isContractDeployed(passkeyNewOwnerAddress)).to.be + .true + }) + + it('should replace any owner of a Safe with a passkey if the passkey contract is deployed', async () => { + const { + accounts: [eoaOwner1], + contractNetworks, + provider, + passkeys: [passkeyNewOwner], + passkeySigners: [passkeySigner] + } = await setupTests() + + const passkeyNewOwnerAddress = await passkeySigner.getAddress() + const safe = await getSafeWithOwners([eoaOwner1.address]) + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + + await deployPasskeysContract([passkeySigner], eoaOwner1.signer) + chai.expect(await safeSdk.getSafeProvider().isContractDeployed(passkeyNewOwnerAddress)).to.be + .true + const currentOwners = await safeSdk.getOwners() + + chai.expect(currentOwners).to.not.include(passkeyNewOwnerAddress) + + const formerOwner = eoaOwner1.address + const swapOwnerTx = await safeSdk.createSwapOwnerTx({ + oldOwnerAddress: formerOwner, + newOwnerPasskey: passkeyNewOwner + }) + + const result = await safeSdk.executeTransaction(swapOwnerTx) + await waitSafeTxReceipt(result) + + const newOwners = await safeSdk.getOwners() + + chai.expect(newOwners).to.include.members([passkeyNewOwnerAddress]) + }) + + it('should replace any passkey owner of a Safe', async () => { + const { + accounts: [eoaOwner1, newEoaOwner], + contractNetworks, + provider, + passkeys: [passkeyOwner1, passkeyOwner2], + passkeySigners: [passkeySigner1, passkeySigner2] + } = await setupTests() + + const passkeyOwner1Address = await passkeySigner1.getAddress() + const passkeyOwner2Address = await passkeySigner2.getAddress() + + await deployPasskeysContract([passkeySigner1, passkeySigner2], eoaOwner1.signer) + const safe = await getSafeWithOwners( + [passkeyOwner1Address, passkeyOwner2Address, eoaOwner1.address], + 2 + ) + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks, + signer: passkeyOwner1 + }) + + const currentOwners = await safeSdk.getOwners() + + chai + .expect(currentOwners) + .to.include.members([passkeyOwner1Address, passkeyOwner2Address, eoaOwner1.address]) + chai.expect(currentOwners).to.not.include(newEoaOwner.address) + + const swapOwnerTx = await safeSdk.createSwapOwnerTx({ + oldOwnerPasskey: passkeyOwner2, + newOwnerAddress: newEoaOwner.address + }) + + const signedTx = await safeSdk.signTransaction(swapOwnerTx) + + const approverSdk = await safeSdk.connect({ + signer: eoaOwner1.address + }) + + const approvedTx = await approverSdk.signTransaction(signedTx) + const result = await approverSdk.executeTransaction(approvedTx) + await waitSafeTxReceipt(result) + + const newOwners = await safeSdk.getOwners() + + chai + .expect(newOwners) + .to.include.members([passkeyOwner1Address, eoaOwner1.address, newEoaOwner.address]) + chai.expect(newOwners).to.not.include(passkeyOwner2Address) + }) + + it('should enable a new passkey owner of a Safe to sign transactions', async () => { + const { + accounts: [owner], + contractNetworks, + provider, + passkeys: [passkeyNewOwner] + } = await setupTests() + + const safe = await getSafeWithOwners([owner.address]) + const safeAddress = await safe.getAddress() + + const safeSdk = await Safe.init({ + provider, + safeAddress, + contractNetworks + }) + + const swapOwnerTx = await safeSdk.createSwapOwnerTx({ + oldOwnerAddress: owner.address, + newOwnerPasskey: passkeyNewOwner + }) + const swapOwnerResult = await safeSdk.executeTransaction(swapOwnerTx) + await waitSafeTxReceipt(swapOwnerResult) + + const safeTransactionData = { + to: safeAddress, + value: '0', + data: '0x' + } + + const safeSdk2 = await safeSdk.connect({ + signer: passkeyNewOwner + }) + const tx = await safeSdk2.createTransaction({ transactions: [safeTransactionData] }) + const signedTx = await safeSdk2.signTransaction(tx) + chai.expect(safeSdk.executeTransaction(signedTx)).to.not.be.rejected + }) + }) + + describeif(safeVersionDeployed >= '1.3.0')('createAddOwnerTx', () => { + it('should add a passkey owner to a Safe and keep the same threshold', async () => { + const { + accounts: [account1], + contractNetworks, + provider, + passkeys: [passkey1], + passkeySigners: [passkeySigner1] + } = await setupTests() + const passkeySigner1Address = await passkeySigner1.getAddress() + + // First create transaction for the deployment of the passkey signer + await deployPasskeysContract([passkeySigner1], account1.signer) + + // Passkey signer should be deployed now + chai.expect(await account1.signer.provider.getCode(passkeySigner1Address)).length.to.be.gt(2) + + const safe = await getSafeWithOwners([account1.address]) + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + const initialThreshold = await safeSdk.getThreshold() + const initialOwners = await safeSdk.getOwners() + + chai.expect(initialOwners.length).to.be.eq(1) + chai.expect(initialOwners[0]).to.be.eq(account1.address) + + const tx = await safeSdk.createAddOwnerTx({ passkey: passkey1 }) + + const txResponse = await safeSdk.executeTransaction(tx) + + await waitSafeTxReceipt(txResponse) + + const finalThreshold = await safeSdk.getThreshold() + chai.expect(initialThreshold).to.be.eq(finalThreshold) + const owners = await safeSdk.getOwners() + chai.expect(owners.length).to.be.eq(initialOwners.length + 1) + chai.expect(owners[0]).to.be.eq(passkeySigner1Address) + chai.expect(owners[1]).to.be.eq(account1.address) + }) + + it('should also deploy a passkey signer before adding as an owner if is not deployed yet', async () => { + const { + accounts: [account1], + contractNetworks, + provider, + passkeys: [passkey1], + passkeySigners: [passkeySigner1] + } = await setupTests() + const passkeySigner1Address = await passkeySigner1.getAddress() + + const safe = await getSafeWithOwners([account1.address]) + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + const initialThreshold = await safeSdk.getThreshold() + const initialOwners = await safeSdk.getOwners() + + chai.expect(initialOwners.length).to.be.eq(1) + chai.expect(initialOwners[0]).to.be.eq(account1.address) + + const tx = await safeSdk.createAddOwnerTx({ passkey: passkey1 }) + + // Check that the passkey signer is not deployed yet + chai.expect(await account1.signer.provider.getCode(passkeySigner1Address)).to.be.eq('0x') + + const txResponse = await safeSdk.executeTransaction(tx) + + await waitSafeTxReceipt(txResponse) + + const finalThreshold = await safeSdk.getThreshold() + chai.expect(initialThreshold).to.be.eq(finalThreshold) + const owners = await safeSdk.getOwners() + chai.expect(owners.length).to.be.eq(initialOwners.length + 1) + chai.expect(owners[0]).to.be.eq(passkeySigner1Address) + chai.expect(owners[1]).to.be.eq(account1.address) + + // Passkey signer should be deployed now + chai.expect(await account1.signer.provider.getCode(passkeySigner1Address)).length.to.be.gt(2) + }) + + it('should add a passkey owner and update the threshold', async () => { + const { + accounts: [account1], + contractNetworks, + provider, + passkeys: [passkey1], + passkeySigners: [passkeySigner1] + } = await setupTests() + const passkeySigner1Address = await passkeySigner1.getAddress() + + const safe = await getSafeWithOwners([account1.address]) + const safeSdk = await Safe.init({ + provider, + safeAddress: await safe.getAddress(), + contractNetworks + }) + const newThreshold = 2 + const initialOwners = await safeSdk.getOwners() + + chai.expect(initialOwners.length).to.be.eq(1) + chai.expect(initialOwners[0]).to.be.eq(account1.address) + + const tx = await safeSdk.createAddOwnerTx({ + passkey: passkey1, + threshold: newThreshold + }) + + const txResponse = await safeSdk.executeTransaction(tx) + + await waitSafeTxReceipt(txResponse) + + const finalThreshold = await safeSdk.getThreshold() + chai.expect(newThreshold).to.be.eq(finalThreshold) + const owners = await safeSdk.getOwners() + chai.expect(owners.length).to.be.eq(initialOwners.length + 1) + chai.expect(owners[0]).to.be.eq(passkeySigner1Address) + chai.expect(owners[1]).to.be.eq(account1.address) + }) + }) + + describeif(safeVersionDeployed >= '1.3.0')( + 'when signing the transaction with a passkey owner', + () => { + it('should add a passkey owner to a Safe and keep the same threshold', async () => { + const { + accounts: [account1], + contractNetworks, + provider, + passkeys: [passkey1, passkey2], + passkeySigners: [passkeySigner1, passkeySigner2] + } = await setupTests() + + const passkeySigner1Address = await passkeySigner1.getAddress() + const passkeySigner2Address = await passkeySigner2.getAddress() + const safe = await getSafeWithOwners([passkeySigner1Address]) + + const safeAddress = await safe.getAddress() + + // First create transaction for the deployment of the passkey signer + await deployPasskeysContract([passkeySigner1], account1.signer) + + // Passkey signer should be deployed now + chai + .expect(await account1.signer.provider.getCode(passkeySigner1Address)) + .length.to.be.gt(2) + + // Create a Safe instance with the passkey signer + const safeSdk = await Safe.init({ + provider, + safeAddress, + contractNetworks, + signer: passkey1 + }) + + // Create a transaction to add another passkey owner + const addOwnerTx = await safeSdk.createAddOwnerTx({ passkey: passkey2 }) + + const initialThreshold = await safeSdk.getThreshold() + const initialOwners = await safeSdk.getOwners() + + chai.expect(initialOwners.length).to.be.eq(1) + chai.expect(initialOwners[0]).to.be.eq(passkeySigner1Address) + + // Sign the transaction with the passkey signer + const signedAddOwnerTx = await safeSdk.signTransaction(addOwnerTx) + + // Create a Safe instance with an EOA signer to execute the transaction + const safeSdkEOA = await Safe.init({ + provider, + safeAddress, + contractNetworks + }) + + // The transaction can only be executed by an EOA signer + const txResponse = await safeSdkEOA.executeTransaction(signedAddOwnerTx) + await waitSafeTxReceipt(txResponse) + + const finalThreshold = await safeSdk.getThreshold() + chai.expect(initialThreshold).to.be.eq(finalThreshold) + + const owners = await safeSdk.getOwners() + chai.expect(owners.length).to.be.eq(initialOwners.length + 1) + chai.expect(owners[0]).to.be.eq(passkeySigner2Address) + chai.expect(owners[1]).to.be.eq(passkeySigner1Address) + }) + } + ) +}) diff --git a/packages/protocol-kit/tests/e2e/safeProvider.test.ts b/packages/protocol-kit/tests/e2e/safeProvider.test.ts index 1b1a13c71..a0c0a18ce 100644 --- a/packages/protocol-kit/tests/e2e/safeProvider.test.ts +++ b/packages/protocol-kit/tests/e2e/safeProvider.test.ts @@ -1,3 +1,4 @@ +import { safeVersionDeployed } from '@safe-global/protocol-kit/hardhat/deploy/deploy-contracts' import { SafeVersion } from '@safe-global/safe-core-sdk-types' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' @@ -16,8 +17,29 @@ import { getEip1193Provider, getSafeProviderFromNetwork } from './utils/setupPro import { getAccounts } from './utils/setupTestNetwork' import { SafeProvider } from '@safe-global/protocol-kit/index' import { AbstractSigner, BrowserProvider, JsonRpcProvider } from 'ethers' +import { itif } from './utils/helpers' +import sinon from 'sinon' +import sinonChai from 'sinon-chai' +import { createMockPasskey, getWebAuthnCredentials } from './utils/passkeys' chai.use(chaiAsPromised) +chai.use(sinonChai) + +const webAuthnCredentials = getWebAuthnCredentials() + +if (!global.crypto) { + global.crypto = crypto as unknown as Crypto +} + +Object.defineProperty(global, 'navigator', { + value: { + credentials: { + create: sinon.stub().callsFake(webAuthnCredentials.create.bind(webAuthnCredentials)), + get: sinon.stub().callsFake(webAuthnCredentials.get.bind(webAuthnCredentials)) + } + }, + writable: true +}) describe('Safe contracts', () => { const setupTests = deployments.createFixture(async ({ deployments, getChainId }) => { @@ -35,6 +57,22 @@ describe('Safe contracts', () => { } }) + describe('init', async () => { + itif(safeVersionDeployed < '1.3.0')( + 'should fail for a passkey signer and Safe { + const { provider } = await setupTests() + const passKeySigner = await createMockPasskey('aName') + + chai + .expect(SafeProvider.init(provider, passKeySigner, safeVersionDeployed)) + .to.be.rejectedWith( + 'Current version of the Safe does not support the Passkey signer functionality' + ) + } + ) + }) + describe('getSafeContract', async () => { it('should return an L1 Safe contract from safe-deployments', async () => { const safeProvider = getSafeProviderFromNetwork('mainnet') diff --git a/packages/protocol-kit/tests/e2e/utils/helpers.ts b/packages/protocol-kit/tests/e2e/utils/helpers.ts index c3e7a1264..155804ead 100644 --- a/packages/protocol-kit/tests/e2e/utils/helpers.ts +++ b/packages/protocol-kit/tests/e2e/utils/helpers.ts @@ -1 +1,2 @@ export const itif = (condition: boolean) => (condition ? it : it.skip) +export const describeif = (condition: boolean) => (condition ? describe : describe.skip) diff --git a/packages/protocol-kit/tests/e2e/utils/passkeys.ts b/packages/protocol-kit/tests/e2e/utils/passkeys.ts new file mode 100644 index 000000000..a9152ffb2 --- /dev/null +++ b/packages/protocol-kit/tests/e2e/utils/passkeys.ts @@ -0,0 +1,95 @@ +import { ethers } from 'ethers' +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' +import { PasskeyArgType, PasskeySigner, extractPasskeyCoordinates } from '@safe-global/protocol-kit' +import { WebAuthnCredentials } from './webauthnShim' + +let singleInstance: WebAuthnCredentials + +/** + * This needs to be a singleton by default. The reason for that is that we are adding it to a global reference in the tests. + * Should only be used if running the tests with a randomly generated private key. + * For testing with a static private key, create a new WebAuthnCredentials instance instead and pass the private key as argument to the constructor. + * @returns WebAuthnCredentials singleton instance + */ +export function getWebAuthnCredentials() { + if (!singleInstance) { + singleInstance = new WebAuthnCredentials() + } + + return singleInstance +} + +/** + * Deploys the passkey contract for each of the signers. + * @param passkeys An array of PasskeySigner representing the passkeys to deploy. + * @param signer A signer to deploy the passkey contracts. + * @returns Passkey deployment transactions + */ +export async function deployPasskeysContract( + passkeys: PasskeySigner[], + signer: HardhatEthersSigner +) { + const toDeploy = passkeys.map(async (passkey) => { + const createPasskeySignerTransaction = { + to: await passkey.safeWebAuthnSignerFactoryContract.getAddress(), + value: '0', + data: passkey.encodeCreateSigner() + } + // Deploy the passkey signer + return await signer.sendTransaction(createPasskeySignerTransaction) + }) + + return Promise.all(toDeploy) +} + +/** + * Creates a mock passkey for testing purposes. + * @param name User name used for passkey mock + * @param webAuthnCredentials The credentials instance to use instead of the singleton. This is useful when mocking the passkey with a static private key. + * @returns Passkey arguments + */ +export async function createMockPasskey( + name: string, + webAuthnCredentials?: WebAuthnCredentials +): Promise { + const credentialsInstance = webAuthnCredentials ?? getWebAuthnCredentials() + const passkeyCredential = await credentialsInstance.create({ + publicKey: { + rp: { + name: 'Safe', + id: 'safe.global' + }, + user: { + id: ethers.getBytes(ethers.id(name)), + name: name, + displayName: name + }, + challenge: ethers.toBeArray(Date.now()), + pubKeyCredParams: [{ type: 'public-key', alg: -7 }] + } + }) + + const algorithm = { + name: 'ECDSA', + namedCurve: 'P-256', + hash: { name: 'SHA-256' } + } + const key = await crypto.subtle.importKey( + 'raw', + passkeyCredential.response.getPublicKey(), + algorithm, + true, + ['verify'] + ) + const exportedPublicKey = await crypto.subtle.exportKey('spki', key) + + const rawId = Buffer.from(passkeyCredential.rawId).toString('hex') + const coordinates = await extractPasskeyCoordinates(exportedPublicKey) + + const passkey: PasskeyArgType = { + rawId, + coordinates + } + + return passkey +} diff --git a/packages/protocol-kit/tests/e2e/utils/setupContractNetworks.ts b/packages/protocol-kit/tests/e2e/utils/setupContractNetworks.ts index 8e0fc4f6a..1ad18e713 100644 --- a/packages/protocol-kit/tests/e2e/utils/setupContractNetworks.ts +++ b/packages/protocol-kit/tests/e2e/utils/setupContractNetworks.ts @@ -6,6 +6,8 @@ import { getMultiSend, getMultiSendCallOnly, getSafeSingleton, + getSafeWebAuthnSharedSigner, + getSafeWebAuthnSignerFactory, getSignMessageLib, getSimulateTxAccessor } from './setupContracts' @@ -28,7 +30,15 @@ export async function getContractNetworks(chainId: bigint): Promise => { + const SafeWebAuthnSignerFactoryDeployment = await deployments.get( + safeWebAuthnSignerFactoryDeployed.name + ) + const SafeWebAuthnSignerFactory = await ethers.getContractFactory( + safeWebAuthnSignerFactoryDeployed.name + ) + return { + contract: SafeWebAuthnSignerFactory.attach(SafeWebAuthnSignerFactoryDeployment.address), + abi: SafeWebAuthnSignerFactoryDeployment.abi + } +} + +export const getSafeWebAuthnSharedSigner = async (): Promise<{ + contract: Contract + abi: JsonFragment | JsonFragment[] +}> => { + const SafeWebAuthnSharedSignerDeployment = await deployments.get( + safeWebAuthnSharedSignerDeployed.name + ) + const SafeWebAuthnSharedSigner = await ethers.getContractFactory( + safeWebAuthnSharedSignerDeployed.name + ) + return { + contract: SafeWebAuthnSharedSigner.attach(SafeWebAuthnSharedSignerDeployment.address), + abi: SafeWebAuthnSharedSignerDeployment.abi + } +} + +export const getWebAuthnContract = async (): Promise => { + const WebAuthnContractDeployment = await deployments.get('WebAuthnContract') + const WebAuthn = await ethers.getContractFactory('WebAuthnContract') + return WebAuthn.attach(WebAuthnContractDeployment.address) +} + export const getDailyLimitModule = async (): Promise => { const DailyLimitModuleDeployment = await deployments.get('DailyLimitModule') const DailyLimitModule = await ethers.getContractFactory('DailyLimitModule') diff --git a/packages/protocol-kit/tests/e2e/utils/webauthnShim.ts b/packages/protocol-kit/tests/e2e/utils/webauthnShim.ts new file mode 100644 index 000000000..961e1734e --- /dev/null +++ b/packages/protocol-kit/tests/e2e/utils/webauthnShim.ts @@ -0,0 +1,321 @@ +/** + * This module provides a minimal shim to emulate the Web Authentication API implemented in browsers. This allows us to + * write tests where we create and authenticate WebAuthn credentials that are verified on-chain. + * + * This implementation is inspired by software authenticators found in the Awesome WebAuthn list [1]. + * + * [1]: + */ + +import { p256 } from '@noble/curves/p256' +import { ethers } from 'ethers' +import type { BytesLike } from 'ethers' +import CBOR from 'cbor' + +/** + * Encode bytes using the Base64 URL encoding. + * + * See + * + * @param data data to encode to `base64url` + * @returns the `base64url` encoded data as a string. + */ +export function base64UrlEncode(data: string | Uint8Array | ArrayBuffer): string { + const buffer = + typeof data === 'string' ? Buffer.from(data.replace(/^0x/, ''), 'hex') : Buffer.from(data) + return buffer.toString('base64url') +} + +/** + * Returns the flag for the user verification requirement. + * + * See: + * + * @param userVerification - The user verification requirement. + * @returns The flag for the user verification requirement. + */ +export function userVerificationFlag( + userVerification: UserVerificationRequirement = 'preferred' +): number { + switch (userVerification) { + case 'preferred': + return 0x01 + case 'required': + return 0x04 + default: + throw new Error(`user verification requirement ${userVerification} not supported`) + } +} + +function b2ab(buf: Uint8Array): ArrayBuffer { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) +} + +/** + * Compare the equality of two Uint8Arrays. + * @param a First array. + * @param b Second array. + * @returns Whether the two arrays are equal. + */ +function isEqualArray(a: Uint8Array, b: Uint8Array) { + if (a.length != b.length) return false + for (let i = 0; i < a.length; i++) if (a[i] != b[i]) return false + return true +} + +/** + * Returns the message that gets signed by the WebAuthn credentials. + * + * See + */ +export function encodeWebAuthnSigningMessage( + clientData: { type: 'webauthn.get'; challenge: string; [key: string]: unknown }, + authenticatorData: BytesLike +) { + return ethers.getBytes( + ethers.concat([ + authenticatorData, + ethers.sha256(ethers.toUtf8Bytes(JSON.stringify(clientData))) + ]) + ) +} + +export interface CredentialCreationOptions { + publicKey: PublicKeyCredentialCreationOptions +} + +export type UserVerificationRequirement = 'required' | 'preferred' | 'discouraged' + +/** + * Public key credetial creation options, restricted to a subset of options that this module supports. + * See . + */ +export interface PublicKeyCredentialCreationOptions { + rp: { id: string; name: string } + user: { id: Uint8Array; displayName: string; name: string } + challenge: Uint8Array + pubKeyCredParams: { + type: 'public-key' + alg: number + }[] + attestation?: 'none' + userVerification?: Exclude +} + +export interface CredentialRequestOptions { + publicKey: PublicKeyCredentialRequestOptions +} + +/** + * Public key credetial request options, restricted to a subset of options that this module supports. + * See . + */ +export interface PublicKeyCredentialRequestOptions { + challenge: Uint8Array + rpId: string + allowCredentials: { + type: 'public-key' + id: Uint8Array + }[] + userVerification?: Exclude + attestation?: 'none' +} + +/** + * A created public key credential. See . + */ +export interface PublicKeyCredential { + type: 'public-key' + id: string + rawId: ArrayBuffer + response: AuthenticatorResponse +} + +/** + * The authenticator's response to a client’s request for the creation of a new public key credential. + * See . + */ +export interface AuthenticatorAttestationResponse { + clientDataJSON: ArrayBuffer + attestationObject: ArrayBuffer + getPublicKey: () => ArrayBuffer +} + +/** + * The authenticator's response to a client’s request generation of a new authentication assertion given the WebAuthn Relying Party's challenge. + * See . + */ +export interface AuthenticatorAssertionResponse { + clientDataJSON: ArrayBuffer + authenticatorData: ArrayBuffer + signature: ArrayBuffer + userHandle: ArrayBuffer +} + +class Credential { + public id: string + public rawId: Uint8Array + public pk: bigint + + constructor( + public rp: string, + public user: Uint8Array, + pk?: bigint + ) { + this.pk = pk || p256.utils.normPrivateKeyToScalar(p256.utils.randomPrivateKey()) + this.id = ethers.dataSlice( + ethers.keccak256(ethers.dataSlice(p256.getPublicKey(this.pk, false), 1)), + 12 + ) + this.rawId = ethers.getBytes(this.id) + } + + /** + * Computes the COSE encoded public key for this credential. + * See . + * + * @returns Hex-encoded COSE-encoded public key + */ + public cosePublicKey(): string { + const pubk = p256.getPublicKey(this.pk, false) + const x = pubk.subarray(1, 33) + const y = pubk.subarray(33, 65) + + // + const key = new Map() + // + key.set(-1, 1) // crv = P-256 + key.set(-2, b2ab(x)) + key.set(-3, b2ab(y)) + // + key.set(1, 2) // kty = EC2 + key.set(3, -7) // alg = ES256 (Elliptic curve signature with SHA-256) + + return ethers.hexlify(CBOR.encode(key)) + } +} + +export class WebAuthnCredentials { + credentials: Credential[] = [] + + /** + * Creates a new instance of the WebAuthn credentials. + * @param privateKey The private key to use for the credentials. If not provided, a random key will be generated. + */ + constructor(private privateKey?: bigint) {} + + /** + * This is a shim for `navigator.credentials.create` method. + * See . + * + * @param options The public key credential creation options. + * @returns A public key credential with an attestation response. + */ + public create({ + publicKey + }: CredentialCreationOptions): PublicKeyCredential { + if (!publicKey.pubKeyCredParams.some(({ alg }) => alg === -7)) { + throw new Error('unsupported signature algorithm(s)') + } + + const credential = new Credential(publicKey.rp.id, publicKey.user.id, this.privateKey) + this.credentials.push(credential) + + // + const clientData = { + type: 'webauthn.create', + challenge: base64UrlEncode(publicKey.challenge), + origin: `https://${publicKey.rp.id}` + } + + // + const attestationObject = { + authData: b2ab( + ethers.getBytes( + ethers.solidityPacked( + ['bytes32', 'uint8', 'uint32', 'bytes16', 'uint16', 'bytes', 'bytes'], + [ + ethers.sha256(ethers.toUtf8Bytes(publicKey.rp.id)), + 0x40 + userVerificationFlag(publicKey.userVerification), // flags = attested_data + user_present + 0, // signCount + `0x${'42'.repeat(16)}`, // aaguid + ethers.dataLength(credential.id), + credential.id, + credential.cosePublicKey() + ] + ) + ) + ), + fmt: 'none', + attStmt: {} + } + + return { + id: base64UrlEncode(credential.rawId), + rawId: credential.rawId.slice(), + response: { + clientDataJSON: b2ab(ethers.toUtf8Bytes(JSON.stringify(clientData))), + attestationObject: b2ab(CBOR.encode(attestationObject)), + getPublicKey: () => b2ab(p256.getPublicKey(credential.pk, false)) + }, + type: 'public-key' + } + } + + /** + * This is a shim for `navigator.credentials.get` method. + * See . + * + * @param options The public key credential request options. + * @returns A public key credential with an assertion response. + */ + get({ + publicKey + }: CredentialRequestOptions): PublicKeyCredential { + const credential = publicKey.allowCredentials + .flatMap(({ id }) => this.credentials.filter((c) => isEqualArray(c.rawId, id))) + .at(0) + if (credential === undefined) { + throw new Error('credential not found') + } + + // + const clientData = { + type: 'webauthn.get' as const, + challenge: base64UrlEncode(publicKey.challenge), + origin: `https://${credential.rp}` + } + + // + // Note that we use a constant 0 value for signCount to simplify things: + // > If the authenticator does not implement a signature counter, let the signature counter + // > value remain constant at zero. + const authenticatorData = ethers.solidityPacked( + ['bytes32', 'uint8', 'uint32'], + [ + ethers.sha256(ethers.toUtf8Bytes(credential.rp)), + userVerificationFlag(publicKey.userVerification), // flags = user_present + 0 // signCount + ] + ) + + // + const message = encodeWebAuthnSigningMessage(clientData, authenticatorData) + const signature = p256.sign(message, credential.pk, { + lowS: false, + prehash: true + }) + + return { + id: base64UrlEncode(credential.rawId), + rawId: credential.rawId.slice(), + response: { + clientDataJSON: b2ab(ethers.toUtf8Bytes(JSON.stringify(clientData))), + authenticatorData: b2ab(ethers.getBytes(authenticatorData)), + signature: b2ab(signature.toDERRawBytes(false)), + userHandle: credential.user + }, + type: 'public-key' + } + } +} diff --git a/packages/protocol-kit/tests/e2e/wrapSafeTransactionIntoDeploymentBatch.test.ts b/packages/protocol-kit/tests/e2e/wrapSafeTransactionIntoDeploymentBatch.test.ts index be1f3bb03..c981f13a2 100644 --- a/packages/protocol-kit/tests/e2e/wrapSafeTransactionIntoDeploymentBatch.test.ts +++ b/packages/protocol-kit/tests/e2e/wrapSafeTransactionIntoDeploymentBatch.test.ts @@ -97,7 +97,7 @@ describe('wrapSafeTransactionIntoDeploymentBatch', () => { chai.expect(batchTransaction).to.be.deep.equal({ to: multiSendContractAddress, value: '0', - data: '0x8d80ff0a00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000432003bf50d1ccca81d4f03c1e71820250fbd8b01eb87000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002041688f0b900000000000000000000000031233647996a4e0d623c9ba42ce8538c2531e22b0000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000003f20fb66d809929e59d9ab1e725d307d696b5593000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c2982b9b2561d80ea09400162752e2538d6af73000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001846a761202000000000000000000000000ffcf8fdee72ac11b5c542428b35eef5769c409f000000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + data: '0x8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000043200ce40e41b77818af46da94c81d35aad468aa386e4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002041688f0b9000000000000000000000000880ed85cacd4d9209452170fc4f16d4fddf00c660000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000060457d01a15434df4c05b29aefbb2d94dc8ddbab000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035329e8abf57fbd5c1920ddbfa5df90e0c02396b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001846a761202000000000000000000000000ffcf8fdee72ac11b5c542428b35eef5769c409f000000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }) } ) @@ -131,7 +131,7 @@ describe('wrapSafeTransactionIntoDeploymentBatch', () => { chai.expect(batchTransaction).to.be.deep.equal({ to: multiSendContractAddress, value: '0', - data: '0x8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000043200359d208d80e7049b8128e64a4d94d9d78c9293e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002041688f0b90000000000000000000000008e6332da7ccd5430bfb27df39fbf386b463c31a50000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000085692cd6f0b50e6d48b98153cba504a09564e776000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bf834dc30c3b4a274a34fcba2b9735af097d3769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001846a761202000000000000000000000000ffcf8fdee72ac11b5c542428b35eef5769c409f000000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + data: '0x8d80ff0a00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000432002411ef313e1970ff055b4f9e861a8182c861c2d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002041688f0b900000000000000000000000012d800ba6577c89ea9bd6728ea6ca40bab7114940000000000000000000000000000000000000000000000000000000000000060a98fb6a6903d95297bfda0abd3057d6e6bf929ab54c89dad163c30546c8040410000000000000000000000000000000000000000000000000000000000000164b63e800d00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000008bec390d0b38e898788fa2aa4e50c263c48f84e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008d24dd72b42554fbb658f2997ba6850ada9dde0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001846a761202000000000000000000000000ffcf8fdee72ac11b5c542428b35eef5769c409f000000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }) } ) diff --git a/packages/relay-kit/.env.example b/packages/relay-kit/.env.example index 7b369e076..8fb018f00 100644 --- a/packages/relay-kit/.env.example +++ b/packages/relay-kit/.env.example @@ -1 +1,2 @@ -PRIVATE_KEY= \ No newline at end of file +PRIVATE_KEY= +PASSKEY_PRIVATE_KEY= \ No newline at end of file diff --git a/packages/relay-kit/jest.config.js b/packages/relay-kit/jest.config.js index 716358e7b..fdc6f4885 100644 --- a/packages/relay-kit/jest.config.js +++ b/packages/relay-kit/jest.config.js @@ -5,6 +5,7 @@ const config = { '^.+\\.ts?$': 'ts-jest' }, moduleNameMapper: { + '^@safe-global/protocol-kit/tests/(.*)$': '/../protocol-kit/tests/$1', '^@safe-global/protocol-kit/(.*)$': '/../protocol-kit/src/$1', '^@safe-global/relay-kit/(.*)$': '/src/$1' }, diff --git a/packages/relay-kit/package.json b/packages/relay-kit/package.json index e8f1bbb3c..eaa5f284a 100644 --- a/packages/relay-kit/package.json +++ b/packages/relay-kit/package.json @@ -1,6 +1,6 @@ { "name": "@safe-global/relay-kit", - "version": "3.0.4", + "version": "3.1.0-alpha.2", "description": "SDK for Safe Smart Accounts with support for ERC-4337 and Relay", "main": "dist/src/index.js", "typings": "dist/src/index.d.ts", @@ -39,9 +39,9 @@ }, "dependencies": { "@gelatonetwork/relay-sdk": "^5.5.0", - "@safe-global/protocol-kit": "^4.0.4", - "@safe-global/safe-core-sdk-types": "^5.0.3", - "@safe-global/safe-modules-deployments": "^2.1.1", + "@safe-global/protocol-kit": "^4.1.0-alpha.2", + "@safe-global/safe-core-sdk-types": "^5.1.0-alpha.2", + "@safe-global/safe-modules-deployments": "^2.2.1", "ethers": "^6.13.1" } } diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts index a7dde4ab4..33b049e73 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts @@ -1,6 +1,9 @@ +import crypto from 'crypto' import dotenv from 'dotenv' import { ethers } from 'ethers' import Safe, * as protocolKit from '@safe-global/protocol-kit' +import { WebAuthnCredentials } from '@safe-global/protocol-kit/tests/e2e/utils/webauthnShim' +import { createMockPasskey } from '@safe-global/protocol-kit/tests/e2e/utils/passkeys' import { getAddModulesLibDeployment, getSafe4337ModuleDeployment @@ -296,7 +299,7 @@ describe('Safe4337Pack', () => { approveToPaymasterTransaction ]) - expect(encodeFunctionDataSpy).toHaveBeenNthCalledWith(4, 'multiSend', [multiSendData]) + expect(encodeFunctionDataSpy).toHaveBeenNthCalledWith(3, 'multiSend', [multiSendData]) expect(safeCreateSpy).toHaveBeenCalledWith({ provider: safe4337Pack.protocolKit.getSafeProvider().provider, signer: safe4337Pack.protocolKit.getSafeProvider().signer, @@ -366,7 +369,7 @@ describe('Safe4337Pack', () => { validUntil: 0, maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, - verificationGasLimit: 150000n, + verificationGasLimit: 400000n, preVerificationGas: 105000n }) }) @@ -394,7 +397,7 @@ describe('Safe4337Pack', () => { validUntil: 0, maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, - verificationGasLimit: 150000n, + verificationGasLimit: 400000n, preVerificationGas: 105000n }) }) @@ -452,7 +455,7 @@ describe('Safe4337Pack', () => { validUntil: 0, maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, - verificationGasLimit: 150000n, + verificationGasLimit: 400000n, preVerificationGas: 105000n }) }) @@ -527,12 +530,186 @@ describe('Safe4337Pack', () => { validUntil: 0, maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, - verificationGasLimit: 150000n, + verificationGasLimit: 400000n, preVerificationGas: 105000n }) }) }) + describe('When using a passkey signer', () => { + const SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS = '0x94a4F6affBd8975951142c3999aEAB7ecee555c2' + const CUSTOM_P256_VERIFIER_ADDRESS = '0xcA89CBa4813D5B40AeC6E57A30d0Eeb500d6531b' + const PASSKEY_PRIVATE_KEY = BigInt(process.env.PASSKEY_PRIVATE_KEY!) + jest.setTimeout(120_000) + + let passkey: protocolKit.PasskeyArgType + + beforeAll(async () => { + if (!global.crypto) { + global.crypto = crypto as unknown as Crypto + } + + const webAuthnCredentials = new WebAuthnCredentials(PASSKEY_PRIVATE_KEY) + + passkey = await createMockPasskey('chucknorris', webAuthnCredentials) + + passkey.customVerifierAddress = CUSTOM_P256_VERIFIER_ADDRESS + + Object.defineProperty(global, 'navigator', { + value: { + credentials: { + create: jest + .fn() + .mockImplementation(webAuthnCredentials.create.bind(webAuthnCredentials)), + get: jest.fn().mockImplementation(webAuthnCredentials.get.bind(webAuthnCredentials)) + } + }, + writable: true + }) + }) + + it('should include a passkey configuration transaction to SafeWebAuthnSharedSigner contract in a multiSend call', async () => { + const encodeFunctionDataSpy = jest.spyOn(constants.INTERFACES, 'encodeFunctionData') + const safeCreateSpy = jest.spyOn(Safe, 'init') + + const safe4337Pack = await createSafe4337Pack({ + signer: passkey, + options: { + owners: [fixtures.OWNER_1], + threshold: 1 + } + }) + + const provider = safe4337Pack.protocolKit.getSafeProvider().provider + const safeProvider = await protocolKit.SafeProvider.init(provider, passkey) + const passkeySigner = (await safeProvider.getExternalSigner()) as protocolKit.PasskeySigner + + const passkeyOwnerConfiguration = { + ...passkeySigner.coordinates, + verifiers: CUSTOM_P256_VERIFIER_ADDRESS + } + + const enableModulesData = constants.INTERFACES.encodeFunctionData('enableModules', [ + [safe4337ModuleAddress] + ]) + const passkeyConfigureData = constants.INTERFACES.encodeFunctionData('configure', [ + passkeyOwnerConfiguration + ]) + + const enable4337ModuleTransaction = { + to: addModulesLibAddress, + value: '0', + data: enableModulesData, + operation: OperationType.DelegateCall + } + + const sharedSignerTransaction = { + to: SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, + value: '0', + data: passkeyConfigureData, + operation: OperationType.DelegateCall + } + + const multiSendData = protocolKit.encodeMultiSendData([ + enable4337ModuleTransaction, + sharedSignerTransaction + ]) + + expect(encodeFunctionDataSpy).toHaveBeenNthCalledWith(2, 'configure', [ + passkeyOwnerConfiguration + ]) + expect(encodeFunctionDataSpy).toHaveBeenNthCalledWith(3, 'multiSend', [multiSendData]) + expect(safeCreateSpy).toHaveBeenCalledWith({ + provider: safe4337Pack.protocolKit.getSafeProvider().provider, + signer: passkey, + predictedSafe: { + safeDeploymentConfig: { + safeVersion: constants.DEFAULT_SAFE_VERSION, + saltNonce: undefined + }, + safeAccountConfig: { + owners: [fixtures.OWNER_1, SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS], + threshold: 1, + to: await safe4337Pack.protocolKit.getMultiSendAddress(), + data: constants.INTERFACES.encodeFunctionData('multiSend', [multiSendData]), + fallbackHandler: safe4337ModuleAddress, + paymentToken: ethers.ZeroAddress, + payment: 0, + paymentReceiver: ethers.ZeroAddress + } + } + }) + }) + + it('should allow to sign a SafeOperation', async () => { + const transferUSDC = { + to: fixtures.PAYMASTER_TOKEN_ADDRESS, + data: generateTransferCallData(fixtures.SAFE_ADDRESS_4337_PASSKEY, 100_000n), + value: '0', + operation: 0 + } + + const safe4337Pack = await createSafe4337Pack({ + signer: passkey, + options: { + owners: [], + threshold: 1 + } + }) + + const safeOperation = await safe4337Pack.createTransaction({ + transactions: [transferUSDC] + }) + + const safeOpHash = utils.calculateSafeUserOperationHash( + safeOperation.data, + BigInt(fixtures.CHAIN_ID), + fixtures.MODULE_ADDRESS + ) + + const passkeySignature = await safe4337Pack.protocolKit.signHash(safeOpHash) + + expect(await safe4337Pack.signSafeOperation(safeOperation)).toMatchObject({ + signatures: new Map().set( + SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS.toLowerCase(), + new protocolKit.EthSafeSignature( + SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, + passkeySignature.data, + true + ) + ) + }) + }) + + it('should allow to send an UserOperation to a bundler', async () => { + const transferUSDC = { + to: fixtures.PAYMASTER_TOKEN_ADDRESS, + data: generateTransferCallData(fixtures.SAFE_ADDRESS_4337_PASSKEY, 100_000n), + value: '0', + operation: 0 + } + + const safe4337Pack = await createSafe4337Pack({ + signer: passkey, + options: { + safeAddress: fixtures.SAFE_ADDRESS_4337_PASSKEY + } + }) + + let safeOperation = await safe4337Pack.createTransaction({ + transactions: [transferUSDC] + }) + safeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + await safe4337Pack.executeTransaction({ executable: safeOperation }) + + expect(sendMock).toHaveBeenCalledWith(constants.RPC_4337_CALLS.SEND_USER_OPERATION, [ + utils.userOperationToHexValues(safeOperation.toUserOperation()), + fixtures.ENTRYPOINTS[0] + ]) + }) + }) + it('should allow to sign a SafeOperation', async () => { const transferUSDC = { to: fixtures.PAYMASTER_TOKEN_ADDRESS, @@ -556,7 +733,7 @@ describe('Safe4337Pack', () => { fixtures.OWNER_1.toLowerCase(), new protocolKit.EthSafeSignature( fixtures.OWNER_1, - '0x8ce4849928aef19e8f5cc199e069a451568dcbaca194a86dc953ae24acac3cbb02a458343127b2a52e1af3b99622b2fc8f1bd9957f84828c33940532a94ea3261c', + '0xda808d1e84e6aac5eb50fda331469a108bfdce442fd41501fefaa5b5d648ade406d08a1ca2ca9a5f0ba1a079da001dbee6990189a2cdb054e6c388d5afbd2d9b20', false ) ) @@ -576,7 +753,7 @@ describe('Safe4337Pack', () => { fixtures.OWNER_1.toLowerCase(), new protocolKit.EthSafeSignature( fixtures.OWNER_1, - '0xcb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1c', + '0x975c7ddab3dc06240918a7bde0f543d1b082a8cadeca19d4bc13c30430367fac46c7ef923d9d0051423d1d59d106e5d199a734cd6a472276d54bb04ec7b3796520', false ) ) diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts index b12b23906..024a4114c 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts @@ -2,10 +2,11 @@ import { ethers } from 'ethers' import semverSatisfies from 'semver/functions/satisfies' import Safe, { EthSafeSignature, - SafeProvider, SigningMethod, encodeMultiSendData, - getMultiSendContract + getMultiSendContract, + PasskeySigner, + SafeProvider } from '@safe-global/protocol-kit' import { RelayKitBasePack } from '@safe-global/relay-kit/RelayKitBasePack' import { @@ -19,7 +20,8 @@ import { } from '@safe-global/safe-core-sdk-types' import { getAddModulesLibDeployment, - getSafe4337ModuleDeployment + getSafe4337ModuleDeployment, + getSafeWebAuthnShareSignerDeployment } from '@safe-global/safe-modules-deployments' import EthSafeOperation from './SafeOperation' import { @@ -75,6 +77,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ #ENTRYPOINT_ADDRESS: string #SAFE_4337_MODULE_ADDRESS: string = '0x' + #SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS: string = '0x' #bundlerClient: ethers.JsonRpcProvider @@ -94,7 +97,8 @@ export class Safe4337Pack extends RelayKitBasePack<{ chainId, paymasterOptions, entryPointAddress, - safe4337ModuleAddress + safe4337ModuleAddress, + safeWebAuthnSharedSignerAddress }: Safe4337Options) { super(protocolKit) @@ -104,6 +108,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ this.#paymasterOptions = paymasterOptions this.#ENTRYPOINT_ADDRESS = entryPointAddress this.#SAFE_4337_MODULE_ADDRESS = safe4337ModuleAddress + this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS = safeWebAuthnSharedSignerAddress || '0x' } /** @@ -158,6 +163,8 @@ export class Safe4337Pack extends RelayKitBasePack<{ ) } + let safeWebAuthnSharedSignerAddress = customContracts?.safeWebAuthnSharedSignerAddress + // Existing Safe if ('safeAddress' in options) { protocolKit = await Safe.init({ @@ -198,8 +205,19 @@ export class Safe4337Pack extends RelayKitBasePack<{ throw new Error('Owners and threshold are required to deploy a new Safe') } - let deploymentTo = addModulesLibAddress - let deploymentData = INTERFACES.encodeFunctionData('enableModules', [[safe4337ModuleAddress]]) + const safeVersion = options.safeVersion || DEFAULT_SAFE_VERSION + + // we need to create a batch to setup the 4337 Safe Account + + // first setup transaction: Enable 4337 module + const enable4337ModuleTransaction = { + to: addModulesLibAddress, + value: '0', + data: INTERFACES.encodeFunctionData('enableModules', [[safe4337ModuleAddress]]), + operation: OperationType.DelegateCall // DelegateCall required for enabling the 4337 module + } + + const setupTransactions = [enable4337ModuleTransaction] const isApproveTransactionRequired = !!paymasterOptions && @@ -209,13 +227,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ if (isApproveTransactionRequired) { const { paymasterAddress, amountToApprove = MAX_ERC20_AMOUNT_TO_APPROVE } = paymasterOptions - const enable4337ModulesTransaction = { - to: addModulesLibAddress, - value: '0', - data: INTERFACES.encodeFunctionData('enableModules', [[safe4337ModuleAddress]]), - operation: OperationType.DelegateCall // DelegateCall required for enabling the 4337 module - } - + // second transaction: approve ERC-20 paymaster token const approveToPaymasterTransaction = { to: paymasterOptions.paymasterTokenAddress, data: INTERFACES.encodeFunctionData('approve', [paymasterAddress, amountToApprove]), @@ -223,19 +235,71 @@ export class Safe4337Pack extends RelayKitBasePack<{ operation: OperationType.Call // Call for approve } - const setupBatch = [enable4337ModulesTransaction, approveToPaymasterTransaction] + setupTransactions.push(approveToPaymasterTransaction) + } - const batchData = INTERFACES.encodeFunctionData('multiSend', [ - encodeMultiSendData(setupBatch) - ]) + const safeProvider = await SafeProvider.init(provider, signer, safeVersion) + + // third transaction: passkey support via shared signer SafeWebAuthnSharedSigner + // see: https://github.com/safe-global/safe-modules/blob/main/modules/passkey/contracts/4337/experimental/README.md + const isPasskeySigner = await safeProvider.isPasskeySigner() + + if (isPasskeySigner) { + if (!safeWebAuthnSharedSignerAddress) { + const safeWebAuthnSharedSignerDeployment = getSafeWebAuthnShareSignerDeployment({ + released: true, + version: '0.2.1', + network + }) + safeWebAuthnSharedSignerAddress = + safeWebAuthnSharedSignerDeployment?.networkAddresses[network] + } + + if (!safeWebAuthnSharedSignerAddress) { + throw new Error(`safeWebAuthnSharedSignerAddress not available for chain ${network}`) + } + + const passkeySigner = (await safeProvider.getExternalSigner()) as PasskeySigner + if (!options.owners.includes(safeWebAuthnSharedSignerAddress)) { + options.owners.push(safeWebAuthnSharedSignerAddress) + } + + const passkeyOwnerConfiguration = { + ...passkeySigner.coordinates, + verifiers: passkeySigner.verifierAddress + } + + const sharedSignerTransaction = { + to: safeWebAuthnSharedSignerAddress, + value: '0', + data: INTERFACES.encodeFunctionData('configure', [passkeyOwnerConfiguration]), + operation: OperationType.DelegateCall // DelegateCall required into the SafeWebAuthnSharedSigner instance in order for it to set its configuration. + } + + setupTransactions.push(sharedSignerTransaction) + } + + let deploymentTo + let deploymentData + + const isBatch = setupTransactions.length > 1 + + if (isBatch) { const multiSendContract = await getMultiSendContract({ - safeProvider: new SafeProvider({ provider, signer }), - safeVersion: options.safeVersion || DEFAULT_SAFE_VERSION + safeProvider, + safeVersion }) + const batchData = INTERFACES.encodeFunctionData('multiSend', [ + encodeMultiSendData(setupTransactions) + ]) + deploymentTo = await multiSendContract.getAddress() deploymentData = batchData + } else { + deploymentTo = enable4337ModuleTransaction.to + deploymentData = enable4337ModuleTransaction.data } protocolKit = await Safe.init({ @@ -243,7 +307,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ signer, predictedSafe: { safeDeploymentConfig: { - safeVersion: options.safeVersion || DEFAULT_SAFE_VERSION, + safeVersion, saltNonce: options.saltNonce || undefined }, safeAccountConfig: { @@ -299,7 +363,8 @@ export class Safe4337Pack extends RelayKitBasePack<{ paymasterOptions, bundlerUrl, entryPointAddress: selectedEntryPoint!, - safe4337ModuleAddress + safe4337ModuleAddress, + safeWebAuthnSharedSignerAddress }) } @@ -316,6 +381,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ safeOperation, feeEstimator = new PimlicoFeeEstimator() }: EstimateFeeProps): Promise { + const threshold = await this.protocolKit.getThreshold() const setupEstimationData = await feeEstimator?.setupEstimation?.({ bundlerUrl: this.#BUNDLER_URL, entryPoint: this.#ENTRYPOINT_ADDRESS, @@ -330,7 +396,11 @@ export class Safe4337Pack extends RelayKitBasePack<{ RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS, [ userOperationToHexValues( - addDummySignature(safeOperation.toUserOperation(), await this.protocolKit.getOwners()) + addDummySignature( + safeOperation.toUserOperation(), + this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, + threshold + ) ), this.#ENTRYPOINT_ADDRESS ] @@ -360,7 +430,11 @@ export class Safe4337Pack extends RelayKitBasePack<{ } const paymasterEstimation = await feeEstimator?.getPaymasterEstimation?.({ - userOperation: safeOperation.toUserOperation(), + userOperation: addDummySignature( + safeOperation.toUserOperation(), + this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, + threshold + ), paymasterUrl: this.#paymasterOptions.paymasterUrl, entryPoint: this.#ENTRYPOINT_ADDRESS, sponsorshipPolicyId: this.#paymasterOptions.sponsorshipPolicyId @@ -536,35 +610,57 @@ export class Safe4337Pack extends RelayKitBasePack<{ safeOp = safeOperation } - const owners = await this.protocolKit.getOwners() - const signerAddress = await this.protocolKit.getSafeProvider().getSignerAddress() + const safeProvider = this.protocolKit.getSafeProvider() + const signerAddress = await safeProvider.getSignerAddress() + const isPasskeySigner = await safeProvider.isPasskeySigner() + if (!signerAddress) { throw new Error('There is no signer address available to sign the SafeOperation') } - const addressIsOwner = owners.some( - (owner: string) => signerAddress && owner.toLowerCase() === signerAddress.toLowerCase() - ) + const isOwner = await this.protocolKit.isOwner(signerAddress) + const isSafeDeployed = await this.protocolKit.isSafeDeployed() - if (!addressIsOwner) { + if ((!isOwner && isSafeDeployed) || (!isSafeDeployed && !isPasskeySigner && !isOwner)) { throw new Error('UserOperations can only be signed by Safe owners') } let signature: SafeSignature - if ( - signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA_V4 || - signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA_V3 || - signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA - ) { - signature = await signSafeOp( - safeOp.data, - this.protocolKit.getSafeProvider(), - this.#SAFE_4337_MODULE_ADDRESS - ) - } else { + + if (isPasskeySigner) { const safeOpHash = safeOp.getHash() - signature = await this.protocolKit.signHash(safeOpHash) + // if the Safe is not deployed we force the Shared Signer signature + if (!isSafeDeployed) { + const passkeySignature = await this.protocolKit.signHash(safeOpHash) + // SafeWebAuthnSharedSigner signature + signature = new EthSafeSignature( + this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, + passkeySignature.data, + true // passkeys are contract signatures + ) + } else { + signature = await this.protocolKit.signHash(safeOpHash) + } + } else { + if ( + signingMethod in + [ + SigningMethod.ETH_SIGN_TYPED_DATA_V4, + SigningMethod.ETH_SIGN_TYPED_DATA_V3, + SigningMethod.ETH_SIGN_TYPED_DATA + ] + ) { + signature = await signSafeOp( + safeOp.data, + this.protocolKit.getSafeProvider(), + this.#SAFE_4337_MODULE_ADDRESS + ) + } else { + const safeOpHash = safeOp.getHash() + + signature = await this.protocolKit.signHash(safeOpHash) + } } const signedSafeOperation = new EthSafeOperation(safeOp.toUserOperation(), { diff --git a/packages/relay-kit/src/packs/safe-4337/constants.ts b/packages/relay-kit/src/packs/safe-4337/constants.ts index 04e66c1ef..4b34e35d5 100644 --- a/packages/relay-kit/src/packs/safe-4337/constants.ts +++ b/packages/relay-kit/src/packs/safe-4337/constants.ts @@ -25,7 +25,8 @@ export const INTERFACES = new ethers.Interface([ 'function enableModules(address[])', 'function multiSend(bytes memory transactions) public payable', 'function executeUserOp(address to, uint256 value, bytes data, uint8 operation)', - 'function approve(address _spender, uint256 _value)' + 'function approve(address _spender, uint256 _value)', + 'function configure((uint256 x, uint256 y, uint176 verifiers) signer)' ]) export const ENTRYPOINT_ADDRESS_V06 = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' diff --git a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts b/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts index 0830929ca..d8c0ebdda 100644 --- a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts +++ b/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts @@ -20,10 +20,9 @@ export class PimlicoFeeEstimator implements IFeeEstimator { async adjustEstimation({ userOperation }: EstimateFeeFunctionProps): Promise { return { - callGasLimit: userOperation.callGasLimit + userOperation.callGasLimit / 2n, - verificationGasLimit: - userOperation.verificationGasLimit + userOperation.verificationGasLimit / 2n, - preVerificationGas: userOperation.preVerificationGas + userOperation.preVerificationGas / 20n + callGasLimit: userOperation.callGasLimit + userOperation.callGasLimit / 2n, // +50% + verificationGasLimit: userOperation.verificationGasLimit * 4n, // +300% + preVerificationGas: userOperation.preVerificationGas + userOperation.preVerificationGas / 20n // +5% } } diff --git a/packages/relay-kit/src/packs/safe-4337/testing-utils/fixtures.ts b/packages/relay-kit/src/packs/safe-4337/testing-utils/fixtures.ts index 9b9f3c016..706238353 100644 --- a/packages/relay-kit/src/packs/safe-4337/testing-utils/fixtures.ts +++ b/packages/relay-kit/src/packs/safe-4337/testing-utils/fixtures.ts @@ -8,6 +8,7 @@ export const SAFE_ADDRESS_v1_3_0 = '0x8C35a08Af278518B59D04ddDe3F1b370aD766D22' export const SAFE_ADDRESS_4337_MODULE_NOT_ENABLED = '0xfC82a1e4A045a44527e8b45FC70332C8F66fc32B' export const SAFE_ADDRESS_4337_FALLBACKHANDLER_NOT_ENABLED = '0xA6FDc4e18404E1715D1bC51B07266c91393C6622' +export const SAFE_ADDRESS_4337_PASSKEY = '0x02DCbFD25178b6b8eFb45603D30b5123179117DD' // Safe owned by passkey signer + 4337 module + fallback handler enabled export const SAFE_MODULES_V0_3_0 = '0.3.0' export const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' export const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' diff --git a/packages/relay-kit/src/packs/safe-4337/types.ts b/packages/relay-kit/src/packs/safe-4337/types.ts index a31155fb8..bcbd57920 100644 --- a/packages/relay-kit/src/packs/safe-4337/types.ts +++ b/packages/relay-kit/src/packs/safe-4337/types.ts @@ -44,6 +44,7 @@ export type Safe4337InitOptions = { entryPointAddress?: string safe4337ModuleAddress?: string addModulesLibAddress?: string + safeWebAuthnSharedSignerAddress?: string } options: ExistingSafeOptions | PredictedSafeOptions paymasterOptions?: PaymasterOptions @@ -57,6 +58,7 @@ export type Safe4337Options = { bundlerClient: ethers.JsonRpcProvider entryPointAddress: string safe4337ModuleAddress: string + safeWebAuthnSharedSignerAddress?: string } export type Safe4337CreateTransactionProps = { diff --git a/packages/relay-kit/src/packs/safe-4337/utils.ts b/packages/relay-kit/src/packs/safe-4337/utils.ts index ec6b58f3a..377bccb87 100644 --- a/packages/relay-kit/src/packs/safe-4337/utils.ts +++ b/packages/relay-kit/src/packs/safe-4337/utils.ts @@ -121,21 +121,52 @@ export function userOperationToHexValues(userOperation: UserOperation) { } /** - * This method creates a dummy signature for the SafeOperation based the owners. + * Passkey Dummy client data JSON fields. This can be used for gas estimations, as it pads the fields enough + * to account for variations in WebAuthn implementations. + */ +export const DUMMY_CLIENT_DATA_FIELDS = [ + `"origin":"https://safe.global"`, + `"padding":"This pads the clientDataJSON so that we can leave room for additional implementation specific fields for a more accurate 'preVerificationGas' estimate."` +].join(',') + +/** + * Dummy authenticator data. This can be used for gas estimations, as it ensures that the correct + * authenticator flags are set. + */ +export const DUMMY_AUTHENTICATOR_DATA = new Uint8Array(37) +// Authenticator data is the concatenation of: +// - 32 byte SHA-256 hash of the relying party ID +// - 1 byte for the user verification flag +// - 4 bytes for the signature count +// We fill it all with `0xfe` and set the appropriate user verification flag. +DUMMY_AUTHENTICATOR_DATA.fill(0xfe) +DUMMY_AUTHENTICATOR_DATA[32] = 0x04 + +/** + * This method creates a dummy signature for the SafeOperation based on the Safe threshold. We assume that all owners are passkeys * This is useful for gas estimations * @param userOperation - The user operation - * @param safeOwners - The safe owner addresses - * @returns The user operation with the dummy signature + * @param signer - The signer + * @param threshold - The Safe threshold + * @returns The user operation with the dummy passkey signature */ export function addDummySignature( userOperation: UserOperation, - safeOwners: string[] + signer: string, + threshold: number ): UserOperation { const signatures = [] - for (const owner of safeOwners) { - const dummySignature = `0x000000000000000000000000${owner.slice(2)}000000000000000000000000000000000000000000000000000000000000000001` - signatures.push(new EthSafeSignature(owner, dummySignature)) + for (let i = 0; i < threshold; i++) { + const isContractSignature = true + const passkeySignature = getSignatureBytes({ + authenticatorData: DUMMY_AUTHENTICATOR_DATA, + clientDataFields: DUMMY_CLIENT_DATA_FIELDS, + r: BigInt(`0x${'ec'.repeat(32)}`), + s: BigInt(`0x${'d5a'.repeat(21)}f`) + }) + + signatures.push(new EthSafeSignature(signer, passkeySignature, isContractSignature)) } return { @@ -146,3 +177,51 @@ export function addDummySignature( ) } } + +/** + * Encodes the given WebAuthn signature into a string. This computes the ABI-encoded signature parameters: + * ```solidity + * abi.encode(authenticatorData, clientDataFields, r, s); + * ``` + * + * @param authenticatorData - The authenticator data as a Uint8Array. + * @param clientDataFields - The client data fields as a string. + * @param r - The value of r as a bigint. + * @param s - The value of s as a bigint. + * @returns The encoded string. + */ +export function getSignatureBytes({ + authenticatorData, + clientDataFields, + r, + s +}: { + authenticatorData: Uint8Array + clientDataFields: string + r: bigint + s: bigint +}): string { + // Helper functions + // Convert a number to a 64-byte hex string with padded upto Hex string with 32 bytes + const encodeUint256 = (x: bigint | number) => x.toString(16).padStart(64, '0') + // Calculate the byte size of the dynamic data along with the length parameter alligned to 32 bytes + const byteSize = (data: Uint8Array) => 32 * (Math.ceil(data.length / 32) + 1) // +1 is for the length parameter + // Encode dynamic data padded with zeros if necessary in 32 bytes chunks + const encodeBytes = (data: Uint8Array) => + `${encodeUint256(data.length)}${ethers.hexlify(data).slice(2)}`.padEnd(byteSize(data) * 2, '0') + + // authenticatorData starts after the first four words. + const authenticatorDataOffset = 32 * 4 + // clientDataFields starts immediately after the authenticator data. + const clientDataFieldsOffset = authenticatorDataOffset + byteSize(authenticatorData) + + return ( + '0x' + + encodeUint256(authenticatorDataOffset) + + encodeUint256(clientDataFieldsOffset) + + encodeUint256(r) + + encodeUint256(s) + + encodeBytes(authenticatorData) + + encodeBytes(new TextEncoder().encode(clientDataFields)) + ) +} diff --git a/packages/safe-core-sdk-types/package.json b/packages/safe-core-sdk-types/package.json index 3fbae77f2..1540b258e 100644 --- a/packages/safe-core-sdk-types/package.json +++ b/packages/safe-core-sdk-types/package.json @@ -1,6 +1,6 @@ { "name": "@safe-global/safe-core-sdk-types", - "version": "5.0.3", + "version": "5.1.0-alpha.2", "description": "Safe Core SDK types", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", diff --git a/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts new file mode 100644 index 000000000..41d6fec0d --- /dev/null +++ b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts @@ -0,0 +1,15 @@ +import { Abi } from 'abitype' +import BaseContract, { EstimateGasFunction } from '../common/BaseContract' + +/** + * Represents the base contract type for a Safe WebAuthn Shared Signer contract. + * + * @template SafeWebAuthnSharedSignerContractAbi - The ABI of the Safe WebAuthn Shared Signer contract. + * @type {SafeWebAuthnSahredSignerBaseContract} + */ +export type SafeWebAuthnSharedSignerBaseContract = + BaseContract & { + estimateGas: EstimateGasFunction + } + +export default SafeWebAuthnSharedSignerBaseContract diff --git a/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/index.ts b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/index.ts new file mode 100644 index 000000000..3f5c46273 --- /dev/null +++ b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/index.ts @@ -0,0 +1,5 @@ +import { SafeWebAuthnSharedSignerContract_v0_2_1_Contract } from './v0.2.1/SafeWebAuthnSharedSigner_v0_2_1' + +export * from './v0.2.1/SafeWebAuthnSharedSigner_v0_2_1' + +export type SafeWebAuthnSharedSignerContractType = SafeWebAuthnSharedSignerContract_v0_2_1_Contract diff --git a/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/v0.2.1/SafeWebAuthnSharedSigner_v0_2_1.ts b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/v0.2.1/SafeWebAuthnSharedSigner_v0_2_1.ts new file mode 100644 index 000000000..8a5cc9952 --- /dev/null +++ b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSharedSigner/v0.2.1/SafeWebAuthnSharedSigner_v0_2_1.ts @@ -0,0 +1,35 @@ +import { ExtractAbiFunctionNames, narrow } from 'abitype' +import safeWebAuthnSharedSigner_v0_2_1_ContractArtifacts from '../../assets/SafeWebAuthnSharedSigner/v0.2.1/safe_webauthn_shared_signer' +import SafeWebAuthnSharedSignerBaseContract from '../SafeWebAuthnSharedSignerBaseContract' +import { ContractFunction } from '../../common/BaseContract' + +const safeWebAuthnSharedSigner_v0_2_1_AbiTypes = narrow( + safeWebAuthnSharedSigner_v0_2_1_ContractArtifacts.abi +) + +/** + * Represents the ABI of the Safe WebAuthn Shared Signer contract version 0.2.1. + * + * @type {SafeWebAuthnSharedSignerContract_v0_2_1_Abi} + */ +export type SafeWebAuthnSharedSignerContract_v0_2_1_Abi = + typeof safeWebAuthnSharedSigner_v0_2_1_AbiTypes + +/** + * Represents the function type derived by the given function name from the SafeWebAuthnSharedSigner contract version 0.2.1 ABI. + * + * @template ContractFunctionName - The function name, derived from the ABI. + * @type {SafeWebAuthnSharedSignerContract_v0_2_1_Function} + */ +export type SafeWebAuthnSharedSignerContract_v0_2_1_Function< + ContractFunctionName extends ExtractAbiFunctionNames +> = ContractFunction + +/** + * Represents the contract type for a Safe WebAuthn Shared Signer contract version 0.2.1, defining read and write methods. + * Utilizes the generic SafeWebAuthnSharedSignerBaseContract with the ABI specific to version 0.2.1. + * + * @type {SafeWebAuthnSharedSignerContract_v0_2_1_Contract} + */ +export type SafeWebAuthnSharedSignerContract_v0_2_1_Contract = + SafeWebAuthnSharedSignerBaseContract diff --git a/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts new file mode 100644 index 000000000..b1845e967 --- /dev/null +++ b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts @@ -0,0 +1,16 @@ +import { Abi } from 'abitype' +import BaseContract, { EstimateGasFunction } from '../common/BaseContract' + +/** + * Represents the base contract type for a Safe WebAuthn Signer Factory contract. + * + * @template SafeWebAuthnSignerFactoryContractAbi - The ABI of the Safe WebAuthn Signer Factory contract. + * @type {SafeWebAuthnSignerFactoryBaseContract} + */ +export type SafeWebAuthnSignerFactoryBaseContract< + SafeWebAuthnSignerFactoryContractAbi extends Abi +> = BaseContract & { + estimateGas: EstimateGasFunction +} + +export default SafeWebAuthnSignerFactoryBaseContract diff --git a/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/index.ts b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/index.ts new file mode 100644 index 000000000..521bce928 --- /dev/null +++ b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/index.ts @@ -0,0 +1,6 @@ +import { SafeWebAuthnSignerFactoryContract_v0_2_1_Contract } from './v0.2.1/SafeWebAuthnSignerFactory_v0_2_1' + +export * from './v0.2.1/SafeWebAuthnSignerFactory_v0_2_1' + +export type SafeWebAuthnSignerFactoryContractType = + SafeWebAuthnSignerFactoryContract_v0_2_1_Contract diff --git a/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/v0.2.1/SafeWebAuthnSignerFactory_v0_2_1.ts b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/v0.2.1/SafeWebAuthnSignerFactory_v0_2_1.ts new file mode 100644 index 000000000..168b07940 --- /dev/null +++ b/packages/safe-core-sdk-types/src/contracts/SafeWebAuthnSignerFactory/v0.2.1/SafeWebAuthnSignerFactory_v0_2_1.ts @@ -0,0 +1,35 @@ +import { ExtractAbiFunctionNames, narrow } from 'abitype' +import safeWebAuthnSignerFactory_v0_2_1_ContractArtifacts from '../../assets/SafeWebAuthnSignerFactory/v0.2.1/safe_webauthn_signer_factory' +import SafeWebAuthnSignerFactoryBaseContract from '../SafeWebAuthnSignerFactoryBaseContract' +import { ContractFunction } from '../../common/BaseContract' + +const safeWebAuthnSignerFactory_v0_2_1_AbiTypes = narrow( + safeWebAuthnSignerFactory_v0_2_1_ContractArtifacts.abi +) + +/** + * Represents the ABI of the Safe WebAuthn Signer Factory contract version 0.2.1. + * + * @type {SafeWebAuthnSignerFactoryContract_v0_2_1_Abi} + */ +export type SafeWebAuthnSignerFactoryContract_v0_2_1_Abi = + typeof safeWebAuthnSignerFactory_v0_2_1_AbiTypes + +/** + * Represents the function type derived by the given function name from the SafeWebAuthnSignerFactory contract version 0.2.1 ABI. + * + * @template ContractFunctionName - The function name, derived from the ABI. + * @type {SafeWebAuthnSignerFactoryContract_v0_2_1_Function} + */ +export type SafeWebAuthnSignerFactoryContract_v0_2_1_Function< + ContractFunctionName extends ExtractAbiFunctionNames +> = ContractFunction + +/** + * Represents the contract type for a Safe WebAuthn Signer Factory contract version 0.2.1, defining read and write methods. + * Utilizes the generic SafeWebAuthnSignerFactoryBaseContract with the ABI specific to version 0.2.1. + * + * @type {SafeWebAuthnSignerFactoryContract_v0_2_1_Contract} + */ +export type SafeWebAuthnSignerFactoryContract_v0_2_1_Contract = + SafeWebAuthnSignerFactoryBaseContract diff --git a/packages/safe-core-sdk-types/src/contracts/assets/SafeWebAuthnSharedSigner/v0.2.1/safe_webauthn_shared_signer.ts b/packages/safe-core-sdk-types/src/contracts/assets/SafeWebAuthnSharedSigner/v0.2.1/safe_webauthn_shared_signer.ts new file mode 100644 index 000000000..045223c51 --- /dev/null +++ b/packages/safe-core-sdk-types/src/contracts/assets/SafeWebAuthnSharedSigner/v0.2.1/safe_webauthn_shared_signer.ts @@ -0,0 +1,142 @@ +export default { + contractName: 'SafeWebAuthnSharedSigner', + abi: [ + { + inputs: [], + stateMutability: 'nonpayable', + type: 'constructor' + }, + { + inputs: [], + name: 'NotDelegateCalled', + type: 'error' + }, + { + inputs: [], + name: 'SIGNER_SLOT', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'x', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'y', + type: 'uint256' + }, + { + internalType: 'P256.Verifiers', + name: 'verifiers', + type: 'uint176' + } + ], + internalType: 'struct SafeWebAuthnSharedSigner.Signer', + name: 'signer', + type: 'tuple' + } + ], + name: 'configure', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'getConfiguration', + outputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'x', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'y', + type: 'uint256' + }, + { + internalType: 'P256.Verifiers', + name: 'verifiers', + type: 'uint176' + } + ], + internalType: 'struct SafeWebAuthnSharedSigner.Signer', + name: 'signer', + type: 'tuple' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'message', + type: 'bytes32' + }, + { + internalType: 'bytes', + name: 'signature', + type: 'bytes' + } + ], + name: 'isValidSignature', + outputs: [ + { + internalType: 'bytes4', + name: 'magicValue', + type: 'bytes4' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes', + name: 'data', + type: 'bytes' + }, + { + internalType: 'bytes', + name: 'signature', + type: 'bytes' + } + ], + name: 'isValidSignature', + outputs: [ + { + internalType: 'bytes4', + name: 'magicValue', + type: 'bytes4' + } + ], + stateMutability: 'view', + type: 'function' + } + ] +} as const diff --git a/packages/safe-core-sdk-types/src/contracts/assets/SafeWebAuthnSignerFactory/v0.2.1/safe_webauthn_signer_factory.ts b/packages/safe-core-sdk-types/src/contracts/assets/SafeWebAuthnSignerFactory/v0.2.1/safe_webauthn_signer_factory.ts new file mode 100644 index 000000000..5179b383b --- /dev/null +++ b/packages/safe-core-sdk-types/src/contracts/assets/SafeWebAuthnSignerFactory/v0.2.1/safe_webauthn_signer_factory.ts @@ -0,0 +1,102 @@ +export default { + contractName: 'SafeWebAuthnSignerFactory', + abi: [ + { + inputs: [ + { + internalType: 'uint256', + name: 'x', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'y', + type: 'uint256' + }, + { + internalType: 'P256.Verifiers', + name: 'verifiers', + type: 'uint192' + } + ], + name: 'createSigner', + outputs: [ + { + internalType: 'address', + name: 'signer', + type: 'address' + } + ], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'x', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'y', + type: 'uint256' + }, + { + internalType: 'P256.Verifiers', + name: 'verifiers', + type: 'uint192' + } + ], + name: 'getSigner', + outputs: [ + { + internalType: 'address', + name: 'signer', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'message', + type: 'bytes32' + }, + { + internalType: 'bytes', + name: 'signature', + type: 'bytes' + }, + { + internalType: 'uint256', + name: 'x', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'y', + type: 'uint256' + }, + { + internalType: 'P256.Verifiers', + name: 'verifiers', + type: 'uint192' + } + ], + name: 'isValidSignatureForSigner', + outputs: [ + { + internalType: 'bytes4', + name: 'magicValue', + type: 'bytes4' + } + ], + stateMutability: 'view', + type: 'function' + } + ] +} as const diff --git a/packages/safe-core-sdk-types/src/contracts/assets/index.ts b/packages/safe-core-sdk-types/src/contracts/assets/index.ts index 0f9a997e0..d81729acb 100644 --- a/packages/safe-core-sdk-types/src/contracts/assets/index.ts +++ b/packages/safe-core-sdk-types/src/contracts/assets/index.ts @@ -28,6 +28,9 @@ import signMessageLib_1_4_1_ContractArtifacts from './SignMessageLib/v1.4.1/sign import simulateTxAccessor_1_3_0_ContractArtifacts from './SimulateTxAccessor/v1.3.0/simulate_tx_accessor' import simulateTxAccessor_1_4_1_ContractArtifacts from './SimulateTxAccessor/v1.4.1/simulate_tx_accessor' +import SafeWebAuthnSignerFactory_0_2_1_ContractArtifacts from './SafeWebAuthnSignerFactory/v0.2.1/safe_webauthn_signer_factory' +import SafeWebAuthnSharedSigner_0_2_1_ContractArtifacts from './SafeWebAuthnSharedSigner/v0.2.1/safe_webauthn_shared_signer' + export { compatibilityFallbackHandler_1_3_0_ContractArtifacts, compatibilityFallbackHandler_1_4_1_ContractArtifacts, @@ -50,5 +53,7 @@ export { signMessageLib_1_3_0_ContractArtifacts, signMessageLib_1_4_1_ContractArtifacts, simulateTxAccessor_1_3_0_ContractArtifacts, - simulateTxAccessor_1_4_1_ContractArtifacts + simulateTxAccessor_1_4_1_ContractArtifacts, + SafeWebAuthnSignerFactory_0_2_1_ContractArtifacts, + SafeWebAuthnSharedSigner_0_2_1_ContractArtifacts } diff --git a/packages/safe-core-sdk-types/src/index.ts b/packages/safe-core-sdk-types/src/index.ts index ef9abd2ee..3e1eefef6 100644 --- a/packages/safe-core-sdk-types/src/index.ts +++ b/packages/safe-core-sdk-types/src/index.ts @@ -5,6 +5,8 @@ export * from './contracts/Safe' export * from './contracts/SafeProxyFactory' export * from './contracts/SignMessageLib' export * from './contracts/SimulateTxAccessor' +export * from './contracts/SafeWebAuthnSignerFactory' +export * from './contracts/SafeWebAuthnSharedSigner' export * from './contracts/common/BaseContract' export * from './contracts/assets' export * from './types' diff --git a/packages/sdk-starter-kit/package.json b/packages/sdk-starter-kit/package.json index 00df34d29..a71f52696 100644 --- a/packages/sdk-starter-kit/package.json +++ b/packages/sdk-starter-kit/package.json @@ -37,9 +37,9 @@ }, "dependencies": { "@safe-global/api-kit": "^2.4.4", - "@safe-global/protocol-kit": "^4.0.4", - "@safe-global/relay-kit": "^3.0.4", - "@safe-global/safe-core-sdk-types": "^5.0.3", + "@safe-global/protocol-kit": "^4.1.0-alpha.2", + "@safe-global/relay-kit": "^3.1.0-alpha.2", + "@safe-global/safe-core-sdk-types": "^5.1.0-alpha.2", "ethers": "^6.13.1" } } diff --git a/yarn.lock b/yarn.lock index c74314b98..77656986d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -327,42 +327,6 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@chainsafe/as-sha256@^0.3.1": - version "0.3.1" - resolved "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz" - integrity sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg== - -"@chainsafe/persistent-merkle-tree@^0.4.2": - version "0.4.2" - resolved "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz" - integrity sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ== - dependencies: - "@chainsafe/as-sha256" "^0.3.1" - -"@chainsafe/persistent-merkle-tree@^0.5.0": - version "0.5.0" - resolved "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz" - integrity sha512-l0V1b5clxA3iwQLXP40zYjyZYospQLZXzBVIhhr9kDg/1qHZfzzHw0jj4VPBijfYCArZDlPkRi1wZaV2POKeuw== - dependencies: - "@chainsafe/as-sha256" "^0.3.1" - -"@chainsafe/ssz@^0.10.0": - version "0.10.2" - resolved "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.10.2.tgz" - integrity sha512-/NL3Lh8K+0q7A3LsiFq09YXS9fPE+ead2rr7vM2QK8PLzrNsw3uqrif9bpRX5UxgeRjM+vYi+boCM3+GM4ovXg== - dependencies: - "@chainsafe/as-sha256" "^0.3.1" - "@chainsafe/persistent-merkle-tree" "^0.5.0" - -"@chainsafe/ssz@^0.9.2": - version "0.9.4" - resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.4.tgz#696a8db46d6975b600f8309ad3a12f7c0e310497" - integrity sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ== - dependencies: - "@chainsafe/as-sha256" "^0.3.1" - "@chainsafe/persistent-merkle-tree" "^0.4.2" - case "^1.6.3" - "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" @@ -656,7 +620,7 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.1", "@ethersproject/providers@^5.7.2": +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.2": version "5.7.2" resolved "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz" integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== @@ -1307,139 +1271,141 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@nomicfoundation/ethereumjs-block@5.0.2": - version "5.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.2.tgz" - integrity sha512-hSe6CuHI4SsSiWWjHDIzWhSiAVpzMUcDRpWYzN0T9l8/Rz7xNn3elwVOJ/tAyS0LqL6vitUD78Uk7lQDXZun7Q== - dependencies: - "@nomicfoundation/ethereumjs-common" "4.0.2" - "@nomicfoundation/ethereumjs-rlp" "5.0.2" - "@nomicfoundation/ethereumjs-trie" "6.0.2" - "@nomicfoundation/ethereumjs-tx" "5.0.2" - "@nomicfoundation/ethereumjs-util" "9.0.2" +"@nomicfoundation/ethereumjs-block@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.4.tgz#ff2acb98a86b9290e35e315a6abfb9aebb9cf39e" + integrity sha512-AcyacJ9eX/uPEvqsPiB+WO1ymE+kyH48qGGiGV+YTojdtas8itUTW5dehDSOXEEItWGbbzEJ4PRqnQZlWaPvDw== + dependencies: + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-trie" "6.0.4" + "@nomicfoundation/ethereumjs-tx" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" ethereum-cryptography "0.1.3" - ethers "^5.7.1" -"@nomicfoundation/ethereumjs-blockchain@7.0.2": - version "7.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.2.tgz" - integrity sha512-8UUsSXJs+MFfIIAKdh3cG16iNmWzWC/91P40sazNvrqhhdR/RtGDlFk2iFTGbBAZPs2+klZVzhRX8m2wvuvz3w== - dependencies: - "@nomicfoundation/ethereumjs-block" "5.0.2" - "@nomicfoundation/ethereumjs-common" "4.0.2" - "@nomicfoundation/ethereumjs-ethash" "3.0.2" - "@nomicfoundation/ethereumjs-rlp" "5.0.2" - "@nomicfoundation/ethereumjs-trie" "6.0.2" - "@nomicfoundation/ethereumjs-tx" "5.0.2" - "@nomicfoundation/ethereumjs-util" "9.0.2" - abstract-level "^1.0.3" +"@nomicfoundation/ethereumjs-blockchain@7.0.4": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.4.tgz#b77511b389290b186c8d999e70f4b15c27ef44ea" + integrity sha512-jYsd/kwzbmpnxx86tXsYV8wZ5xGvFL+7/P0c6OlzpClHsbFzeF41KrYA9scON8Rg6bZu3ZTv6JOAgj3t7USUfg== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.4" + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-ethash" "3.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-trie" "6.0.4" + "@nomicfoundation/ethereumjs-tx" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" debug "^4.3.3" ethereum-cryptography "0.1.3" - level "^8.0.0" - lru-cache "^5.1.1" - memory-level "^1.0.0" + lru-cache "^10.0.0" -"@nomicfoundation/ethereumjs-common@4.0.2": - version "4.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.2.tgz" - integrity sha512-I2WGP3HMGsOoycSdOTSqIaES0ughQTueOsddJ36aYVpI3SN8YSusgRFLwzDJwRFVIYDKx/iJz0sQ5kBHVgdDwg== +"@nomicfoundation/ethereumjs-common@4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.4.tgz#9901f513af2d4802da87c66d6f255b510bef5acb" + integrity sha512-9Rgb658lcWsjiicr5GzNCjI1llow/7r0k50dLL95OJ+6iZJcVbi15r3Y0xh2cIO+zgX0WIHcbzIu6FeQf9KPrg== dependencies: - "@nomicfoundation/ethereumjs-util" "9.0.2" - crc-32 "^1.2.0" + "@nomicfoundation/ethereumjs-util" "9.0.4" -"@nomicfoundation/ethereumjs-ethash@3.0.2": - version "3.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.2.tgz" - integrity sha512-8PfoOQCcIcO9Pylq0Buijuq/O73tmMVURK0OqdjhwqcGHYC2PwhbajDh7GZ55ekB0Px197ajK3PQhpKoiI/UPg== - dependencies: - "@nomicfoundation/ethereumjs-block" "5.0.2" - "@nomicfoundation/ethereumjs-rlp" "5.0.2" - "@nomicfoundation/ethereumjs-util" "9.0.2" - abstract-level "^1.0.3" - bigint-crypto-utils "^3.0.23" +"@nomicfoundation/ethereumjs-ethash@3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.4.tgz#06cb2502b3012fb6c11cffd44af08aecf71310da" + integrity sha512-xvIrwIMl9sSaiYKRem68+O7vYdj7Q2XWv5P7JXiIkn83918QzWHvqbswTRsH7+r6X1UEvdsURRnZbvZszEjAaQ== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" + bigint-crypto-utils "^3.2.2" ethereum-cryptography "0.1.3" -"@nomicfoundation/ethereumjs-evm@2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.2.tgz" - integrity sha512-rBLcUaUfANJxyOx9HIdMX6uXGin6lANCulIm/pjMgRqfiCRMZie3WKYxTSd8ZE/d+qT+zTedBF4+VHTdTSePmQ== - dependencies: - "@ethersproject/providers" "^5.7.1" - "@nomicfoundation/ethereumjs-common" "4.0.2" - "@nomicfoundation/ethereumjs-tx" "5.0.2" - "@nomicfoundation/ethereumjs-util" "9.0.2" +"@nomicfoundation/ethereumjs-evm@2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.4.tgz#c9c761767283ac53946185474362230b169f8f63" + integrity sha512-lTyZZi1KpeMHzaO6cSVisR2tjiTTedjo7PcmhI/+GNFo9BmyY6QYzGeSti0sFttmjbEMioHgXxl5yrLNRg6+1w== + dependencies: + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-statemanager" "2.0.4" + "@nomicfoundation/ethereumjs-tx" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" + "@types/debug" "^4.1.9" debug "^4.3.3" ethereum-cryptography "0.1.3" - mcl-wasm "^0.7.1" - rustbn.js "~0.2.0" + rustbn-wasm "^0.2.0" -"@nomicfoundation/ethereumjs-rlp@5.0.2": - version "5.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.2.tgz" - integrity sha512-QwmemBc+MMsHJ1P1QvPl8R8p2aPvvVcKBbvHnQOKBpBztEo0omN0eaob6FeZS/e3y9NSe+mfu3nNFBHszqkjTA== +"@nomicfoundation/ethereumjs-rlp@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.4.tgz#66c95256fc3c909f6fb18f6a586475fc9762fa30" + integrity sha512-8H1S3s8F6QueOc/X92SdrA4RDenpiAEqMg5vJH99kcQaCy/a3Q6fgseo75mgWlbanGJXSlAPtnCeG9jvfTYXlw== -"@nomicfoundation/ethereumjs-statemanager@2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.2.tgz" - integrity sha512-dlKy5dIXLuDubx8Z74sipciZnJTRSV/uHG48RSijhgm1V7eXYFC567xgKtsKiVZB1ViTP9iFL4B6Je0xD6X2OA== +"@nomicfoundation/ethereumjs-statemanager@2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.4.tgz#bf14415e1f31b5ea8b98a0c027c547d0555059b6" + integrity sha512-HPDjeFrxw6llEi+BzqXkZ+KkvFnTOPczuHBtk21hRlDiuKuZz32dPzlhpRsDBGV1b5JTmRDUVqCS1lp3Gghw4Q== dependencies: - "@nomicfoundation/ethereumjs-common" "4.0.2" - "@nomicfoundation/ethereumjs-rlp" "5.0.2" + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-trie" "6.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" debug "^4.3.3" ethereum-cryptography "0.1.3" - ethers "^5.7.1" js-sdsl "^4.1.4" + lru-cache "^10.0.0" -"@nomicfoundation/ethereumjs-trie@6.0.2": - version "6.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.2.tgz" - integrity sha512-yw8vg9hBeLYk4YNg5MrSJ5H55TLOv2FSWUTROtDtTMMmDGROsAu+0tBjiNGTnKRi400M6cEzoFfa89Fc5k8NTQ== +"@nomicfoundation/ethereumjs-trie@6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.4.tgz#688a3f76646c209365ee6d959c3d7330ede5e609" + integrity sha512-3nSwQiFMvr2VFe/aZUyinuohYvtytUqZCUCvIWcPJ/BwJH6oQdZRB42aNFBJ/8nAh2s3OcroWpBLskzW01mFKA== dependencies: - "@nomicfoundation/ethereumjs-rlp" "5.0.2" - "@nomicfoundation/ethereumjs-util" "9.0.2" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" "@types/readable-stream" "^2.3.13" ethereum-cryptography "0.1.3" + lru-cache "^10.0.0" readable-stream "^3.6.0" -"@nomicfoundation/ethereumjs-tx@5.0.2": - version "5.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.2.tgz" - integrity sha512-T+l4/MmTp7VhJeNloMkM+lPU3YMUaXdcXgTGCf8+ZFvV9NYZTRLFekRwlG6/JMmVfIfbrW+dRRJ9A6H5Q/Z64g== +"@nomicfoundation/ethereumjs-tx@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.4.tgz#b0ceb58c98cc34367d40a30d255d6315b2f456da" + integrity sha512-Xjv8wAKJGMrP1f0n2PeyfFCCojHd7iS3s/Ab7qzF1S64kxZ8Z22LCMynArYsVqiFx6rzYy548HNVEyI+AYN/kw== dependencies: - "@chainsafe/ssz" "^0.9.2" - "@ethersproject/providers" "^5.7.2" - "@nomicfoundation/ethereumjs-common" "4.0.2" - "@nomicfoundation/ethereumjs-rlp" "5.0.2" - "@nomicfoundation/ethereumjs-util" "9.0.2" + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" ethereum-cryptography "0.1.3" -"@nomicfoundation/ethereumjs-util@9.0.2": - version "9.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.2.tgz" - integrity sha512-4Wu9D3LykbSBWZo8nJCnzVIYGvGCuyiYLIJa9XXNVt1q1jUzHdB+sJvx95VGCpPkCT+IbLecW6yfzy3E1bQrwQ== +"@nomicfoundation/ethereumjs-util@9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.4.tgz#84c5274e82018b154244c877b76bc049a4ed7b38" + integrity sha512-sLOzjnSrlx9Bb9EFNtHzK/FJFsfg2re6bsGqinFinH1gCqVfz9YYlXiMWwDM4C/L4ywuHFCYwfKTVr/QHQcU0Q== dependencies: - "@chainsafe/ssz" "^0.10.0" - "@nomicfoundation/ethereumjs-rlp" "5.0.2" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" ethereum-cryptography "0.1.3" -"@nomicfoundation/ethereumjs-vm@7.0.2": - version "7.0.2" - resolved "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.2.tgz" - integrity sha512-Bj3KZT64j54Tcwr7Qm/0jkeZXJMfdcAtRBedou+Hx0dPOSIgqaIr0vvLwP65TpHbak2DmAq+KJbW2KNtIoFwvA== - dependencies: - "@nomicfoundation/ethereumjs-block" "5.0.2" - "@nomicfoundation/ethereumjs-blockchain" "7.0.2" - "@nomicfoundation/ethereumjs-common" "4.0.2" - "@nomicfoundation/ethereumjs-evm" "2.0.2" - "@nomicfoundation/ethereumjs-rlp" "5.0.2" - "@nomicfoundation/ethereumjs-statemanager" "2.0.2" - "@nomicfoundation/ethereumjs-trie" "6.0.2" - "@nomicfoundation/ethereumjs-tx" "5.0.2" - "@nomicfoundation/ethereumjs-util" "9.0.2" +"@nomicfoundation/ethereumjs-verkle@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-verkle/-/ethereumjs-verkle-0.0.2.tgz#7686689edec775b2efea5a71548f417c18f7dea4" + integrity sha512-bjnfZElpYGK/XuuVRmLS3yDvr+cDs85D9oonZ0YUa5A3lgFgokWMp76zXrxX2jVQ0BfHaw12y860n1+iOi6yFQ== + dependencies: + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" + lru-cache "^10.0.0" + rust-verkle-wasm "^0.0.1" + +"@nomicfoundation/ethereumjs-vm@7.0.4": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.4.tgz#e5a6eec4877dc62dda93003c6d7afd1fe4b9625b" + integrity sha512-gsA4IhmtWHI4BofKy3kio9W+dqZQs5Ji5mLjLYxHCkat+JQBUt5szjRKra2F9nGDJ2XcI/wWb0YWUFNgln4zRQ== + dependencies: + "@nomicfoundation/ethereumjs-block" "5.0.4" + "@nomicfoundation/ethereumjs-blockchain" "7.0.4" + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-evm" "2.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-statemanager" "2.0.4" + "@nomicfoundation/ethereumjs-trie" "6.0.4" + "@nomicfoundation/ethereumjs-tx" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" debug "^4.3.3" ethereum-cryptography "0.1.3" - mcl-wasm "^0.7.1" - rustbn.js "~0.2.0" "@nomicfoundation/hardhat-ethers@^3.0.6": version "3.0.6" @@ -1825,6 +1791,11 @@ resolved "https://registry.npmjs.org/@safe-global/safe-contracts/-/safe-contracts-1.4.1.tgz" integrity sha512-fP1jewywSwsIniM04NsqPyVRFKPMAuirC3ftA/TA4X3Zc5EnwQp/UCJUU2PL/37/z/jMo8UUaJ+pnFNWmMU7dQ== +"@safe-global/safe-contracts@^1.4.1-build.0": + version "1.4.1-build.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-contracts/-/safe-contracts-1.4.1-build.0.tgz#5d82e2f3fd8430b4589df992b9ee2c71386082fe" + integrity sha512-TIpoKJtMqLcLFoid0cvpxo8YTcnRUj95MHvxzwgPbJPRONOckNS6ebgGyBBRDmnpxFh34IBpPUZ7JD+z2Cfbbg== + "@safe-global/safe-deployments@^1.37.3": version "1.37.3" resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.37.3.tgz#ded9fa6bb04f0e8972c00c481badcf513d590b0b" @@ -1832,12 +1803,20 @@ dependencies: semver "^7.6.2" -"@safe-global/safe-modules-deployments@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@safe-global/safe-modules-deployments/-/safe-modules-deployments-2.1.1.tgz#ee887d7349dc6b8e9caa944cd612e71a73b5c8ac" - integrity sha512-Tfiv+qYGEJM7idF8Ee1Gu8thg3qkCONBlOQxjS45kTAWn2VfnaPgIH2aMguiXbhkU8LgbNTzWtmmib+dR7KGpQ== +"@safe-global/safe-modules-deployments@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-modules-deployments/-/safe-modules-deployments-2.2.1.tgz#a8b88f2afc6ec04fed09968fe1e4990ed975c86e" + integrity sha512-H0XpusyXVcsTuRsQSq0FoBKqRfhZH87/1DrFEmXXPXmI3fJkvxq3KpTaTTqzcqoIe/J+erwVZQUYNfL68EcvAQ== -"@scure/base@^1.1.3", "@scure/base@~1.1.0", "@scure/base@~1.1.2": +"@safe-global/safe-passkey@0.2.0-alpha.1": + version "0.2.0-alpha.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-passkey/-/safe-passkey-0.2.0-alpha.1.tgz#a9e80e727ffe836f656808595dd4d59438948294" + integrity sha512-rmxZH79J0ynQf9WqyWHbHWbv1K7s6X1hFpp55f4euMO9taoU7Ymw5p+8iBgQNe4EWy4NjRbtfK9NKD7JaLoYjQ== + dependencies: + "@safe-global/safe-contracts" "^1.4.1-build.0" + cbor "^9.0.2" + +"@scure/base@^1.1.1", "@scure/base@^1.1.3", "@scure/base@~1.1.0", "@scure/base@~1.1.2": version "1.1.6" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== @@ -2305,7 +2284,7 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.16.tgz#b1572967f0b8b60bf3f87fe1d854a5604ea70c82" integrity sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ== -"@types/debug@^4.1.7": +"@types/debug@^4.1.7", "@types/debug@^4.1.9": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== @@ -2419,7 +2398,7 @@ "@types/readable-stream@^2.3.13": version "2.3.15" - resolved "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.15.tgz#3d79c9ceb1b6a57d5f6e6976f489b9b5384321ae" integrity sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ== dependencies: "@types/node" "*" @@ -2653,19 +2632,6 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -abstract-level@^1.0.0, abstract-level@^1.0.2, abstract-level@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.3.tgz" - integrity sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA== - dependencies: - buffer "^6.0.3" - catering "^2.1.0" - is-buffer "^2.0.5" - level-supports "^4.0.0" - level-transcoder "^1.0.1" - module-error "^1.0.1" - queue-microtask "^1.2.3" - acorn-globals@^7.0.0: version "7.0.1" resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz" @@ -2750,6 +2716,13 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" @@ -3024,10 +2997,10 @@ before-after-hook@^2.2.0: resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== -bigint-crypto-utils@^3.0.23: - version "3.2.2" - resolved "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.2.2.tgz" - integrity sha512-U1RbE3aX9ayCUVcIPHuPDPKcK3SFOXf93J1UK/iHlJuQB7bhagPIX06/CLpLEsDThJ7KA4Dhrnzynl+d2weTiw== +bigint-crypto-utils@^3.2.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/bigint-crypto-utils/-/bigint-crypto-utils-3.3.0.tgz#72ad00ae91062cf07f2b1def9594006c279c1d77" + integrity sha512-jOTSb+drvEDxEq6OuUybOAv/xxoh3cuYRUIPyu8sSHQNKM303UQ2R1DAo45o1AkcIXw6fzbaFI1+xGGdaXs2lg== bignumber.js@^9.1.2: version "9.1.2" @@ -3068,6 +3041,20 @@ bowser@^2.11.0: resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz" integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== +boxen@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -3095,16 +3082,6 @@ brorand@^1.1.0: resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== -browser-level@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/browser-level/-/browser-level-1.0.1.tgz" - integrity sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ== - dependencies: - abstract-level "^1.0.2" - catering "^2.1.1" - module-error "^1.0.2" - run-parallel-limit "^1.1.0" - browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" @@ -3295,15 +3272,12 @@ caniuse-lite@^1.0.30001541: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001563.tgz" integrity sha512-na2WUmOxnwIZtwnFI2CZ/3er0wdNzU7hN+cPYz/z2ajHThnkWjNBOpEPP4n+4r2WPM847JaMotaJE3bnfzjyKw== -case@^1.6.3: - version "1.6.3" - resolved "https://registry.npmjs.org/case/-/case-1.6.3.tgz" - integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== - -catering@^2.1.0, catering@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz" - integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== +cbor@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/cbor/-/cbor-9.0.2.tgz#536b4f2d544411e70ec2b19a2453f10f83cd9fdb" + integrity sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ== + dependencies: + nofilter "^3.1.0" chai-as-promised@^7.1.1: version "7.1.1" @@ -3415,22 +3389,16 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -classic-level@^1.2.0: - version "1.3.0" - resolved "https://registry.npmjs.org/classic-level/-/classic-level-1.3.0.tgz" - integrity sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg== - dependencies: - abstract-level "^1.0.2" - catering "^2.1.0" - module-error "^1.0.1" - napi-macros "^2.2.2" - node-gyp-build "^4.3.0" - clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + cli-cursor@3.1.0, cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" @@ -4377,7 +4345,7 @@ ethers@6.7.0: tslib "2.4.0" ws "8.5.0" -ethers@^5.7.0, ethers@^5.7.1: +ethers@^5.7.0, ethers@~5.7.0: version "5.7.2" resolved "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -4808,11 +4776,6 @@ function-bind@^1.1.1, function-bind@^1.1.2: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - gauge@^4.0.3: version "4.0.4" resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz" @@ -5073,10 +5036,10 @@ hardhat-deploy-ethers@^0.4.2: resolved "https://registry.yarnpkg.com/hardhat-deploy-ethers/-/hardhat-deploy-ethers-0.4.2.tgz#10aa44ef806ec8cf3d67ad9692f3762ed965b5e7" integrity sha512-AskNH/XRYYYqPT94MvO5s1yMi+/QvoNjS4oU5VcVqfDU99kgpGETl+uIYHIrSXtH5sy7J6gyVjpRMf4x0tjLSQ== -hardhat-deploy@^0.11.45: - version "0.11.45" - resolved "https://registry.yarnpkg.com/hardhat-deploy/-/hardhat-deploy-0.11.45.tgz#bed86118175a38a03bb58aba2ce1ed5e80a20bc8" - integrity sha512-aC8UNaq3JcORnEUIwV945iJuvBwi65tjHVDU3v6mOcqik7WAzHVCJ7cwmkkipsHrWysrB5YvGF1q9S1vIph83w== +hardhat-deploy@^0.12.4: + version "0.12.4" + resolved "https://registry.yarnpkg.com/hardhat-deploy/-/hardhat-deploy-0.12.4.tgz#5ebef37f1004f52a74987213b0465ad7c9433fb2" + integrity sha512-bYO8DIyeGxZWlhnMoCBon9HNZb6ji0jQn7ngP1t5UmGhC8rQYhji7B73qETMOFhzt5ECZPr+U52duj3nubsqdQ== dependencies: "@ethersproject/abi" "^5.7.0" "@ethersproject/abstract-signer" "^5.7.0" @@ -5101,25 +5064,26 @@ hardhat-deploy@^0.11.45: match-all "^1.2.6" murmur-128 "^0.2.1" qs "^6.9.4" - zksync-web3 "^0.14.3" + zksync-ethers "^5.0.0" -hardhat@^2.19.3: - version "2.19.4" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.19.4.tgz#5112c30295d8be2e18e55d847373c50483ed1902" - integrity sha512-fTQJpqSt3Xo9Mn/WrdblNGAfcANM6XC3tAEi6YogB4s02DmTf93A8QsGb8uR0KR8TFcpcS8lgiW4ugAIYpnbrQ== +hardhat@2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.20.1.tgz#3ad8f2b003a96c9ce80a55fec3575580ff2ddcd4" + integrity sha512-q75xDQiQtCZcTMBwjTovrXEU5ECr49baxr4/OBkIu/ULTPzlB20yk1dRWNmD2IFbAeAeXggaWvQAdpiScaHtPw== dependencies: "@ethersproject/abi" "^5.1.2" "@metamask/eth-sig-util" "^4.0.0" - "@nomicfoundation/ethereumjs-block" "5.0.2" - "@nomicfoundation/ethereumjs-blockchain" "7.0.2" - "@nomicfoundation/ethereumjs-common" "4.0.2" - "@nomicfoundation/ethereumjs-evm" "2.0.2" - "@nomicfoundation/ethereumjs-rlp" "5.0.2" - "@nomicfoundation/ethereumjs-statemanager" "2.0.2" - "@nomicfoundation/ethereumjs-trie" "6.0.2" - "@nomicfoundation/ethereumjs-tx" "5.0.2" - "@nomicfoundation/ethereumjs-util" "9.0.2" - "@nomicfoundation/ethereumjs-vm" "7.0.2" + "@nomicfoundation/ethereumjs-block" "5.0.4" + "@nomicfoundation/ethereumjs-blockchain" "7.0.4" + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-evm" "2.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-statemanager" "2.0.4" + "@nomicfoundation/ethereumjs-trie" "6.0.4" + "@nomicfoundation/ethereumjs-tx" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" + "@nomicfoundation/ethereumjs-verkle" "0.0.2" + "@nomicfoundation/ethereumjs-vm" "7.0.4" "@nomicfoundation/solidity-analyzer" "^0.1.0" "@sentry/node" "^5.18.1" "@types/bn.js" "^5.1.0" @@ -5127,6 +5091,7 @@ hardhat@^2.19.3: adm-zip "^0.4.16" aggregate-error "^3.0.0" ansi-escapes "^4.3.0" + boxen "^5.1.2" chalk "^2.4.2" chokidar "^3.4.0" ci-info "^2.0.0" @@ -5526,11 +5491,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-buffer@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - is-callable@^1.1.3: version "1.2.7" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" @@ -6229,9 +6189,9 @@ jest@^29.7.0: jest-cli "^29.7.0" js-sdsl@^4.1.4: - version "4.4.0" - resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz" - integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg== + version "4.4.2" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847" + integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== js-sha3@0.8.0: version "0.8.0" @@ -6499,27 +6459,6 @@ lerna@^8.1.3: yargs "17.7.2" yargs-parser "21.1.1" -level-supports@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/level-supports/-/level-supports-4.0.1.tgz" - integrity sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA== - -level-transcoder@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz" - integrity sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w== - dependencies: - buffer "^6.0.3" - module-error "^1.0.1" - -level@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/level/-/level-8.0.0.tgz" - integrity sha512-ypf0jjAk2BWI33yzEaaotpq7fkOPALKAgDBxggO6Q9HGX2MRXn0wbP1Jn/tJv1gtL867+YOjOB49WaUF3UoJNQ== - dependencies: - browser-level "^1.0.1" - classic-level "^1.2.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" @@ -6721,10 +6660,10 @@ loupe@^2.3.6: dependencies: get-func-name "^2.0.0" -lru-cache@^10.0.1, lru-cache@^10.2.0: - version "10.2.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" - integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== +lru-cache@^10.0.0, lru-cache@^10.0.1, lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== lru-cache@^5.1.1: version "5.1.1" @@ -6838,11 +6777,6 @@ match-all@^1.2.6: resolved "https://registry.npmjs.org/match-all/-/match-all-1.2.6.tgz" integrity sha512-0EESkXiTkWzrQQntBu2uzKvLu6vVkUGz40nGPbSZuegcfE5UuSzNjLaIu76zJWuaT/2I3Z/8M06OlUOZLGwLlQ== -mcl-wasm@^0.7.1: - version "0.7.9" - resolved "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-0.7.9.tgz" - integrity sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ== - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz" @@ -6852,15 +6786,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -memory-level@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/memory-level/-/memory-level-1.0.0.tgz" - integrity sha512-UXzwewuWeHBz5krr7EvehKcmLFNoXxGcvuYhC41tRnkrTbJohtS7kVn9akmgirtRygg+f7Yjsfi8Uu5SGSQ4Og== - dependencies: - abstract-level "^1.0.0" - functional-red-black-tree "^1.0.1" - module-error "^1.0.1" - memorystream@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz" @@ -7134,11 +7059,6 @@ modify-values@^1.0.1: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -module-error@^1.0.1, module-error@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz" - integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== - ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" @@ -7189,11 +7109,6 @@ nanoid@3.3.3: resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz" integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -napi-macros@^2.2.2: - version "2.2.2" - resolved "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz" - integrity sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" @@ -7239,7 +7154,7 @@ node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.7.0: dependencies: whatwg-url "^5.0.0" -node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: +node-gyp-build@^4.2.0: version "4.6.0" resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz" integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== @@ -7282,6 +7197,11 @@ node-releases@^2.0.13: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz" integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== +nofilter@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-3.1.0.tgz#c757ba68801d41ff930ba2ec55bab52ca184aa66" + integrity sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g== + nopt@^7.0.0: version "7.2.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7" @@ -8130,7 +8050,7 @@ queue-lit@^1.5.0: resolved "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.0.tgz" integrity sha512-IslToJ4eiCEE9xwMzq3viOO5nH8sUWUCwoElrhNMozzr9IIt2qqvB4I+uHu/zJTQVqc9R5DFwok4ijNK1pU3fA== -queue-microtask@^1.2.2, queue-microtask@^1.2.3: +queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== @@ -8443,13 +8363,6 @@ run-async@^2.4.0: resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -run-parallel-limit@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz" - integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== - dependencies: - queue-microtask "^1.2.2" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" @@ -8457,10 +8370,17 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rustbn.js@~0.2.0: +rust-verkle-wasm@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/rust-verkle-wasm/-/rust-verkle-wasm-0.0.1.tgz#fd8396a7060d8ee8ea10da50ab6e862948095a74" + integrity sha512-BN6fiTsxcd2dCECz/cHtGTt9cdLJR925nh7iAuRcj8ymKw7OOaPmCneQZ7JePOJ/ia27TjEL91VdOi88Yf+mcA== + +rustbn-wasm@^0.2.0: version "0.2.0" - resolved "https://registry.npmjs.org/rustbn.js/-/rustbn.js-0.2.0.tgz" - integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== + resolved "https://registry.yarnpkg.com/rustbn-wasm/-/rustbn-wasm-0.2.0.tgz#0407521fb55ae69eeb4968d01885d63efd1c4ff9" + integrity sha512-FThvYFNTqrEKGqXuseeg0zR7yROh/6U1617mCHF68OVqrN1tNKRN7Tdwy4WayPVsCmmK+eMxtIZX1qL6JxTkMg== + dependencies: + "@scure/base" "^1.1.1" rxjs@^7.5.5: version "7.8.0" @@ -8880,7 +8800,7 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9810,6 +9730,13 @@ wide-align@^1.1.5: dependencies: string-width "^1.0.2 || 2 || 3 || 4" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + word-wrap@~1.2.3: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" @@ -10087,10 +10014,12 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zksync-web3@^0.14.3: - version "0.14.3" - resolved "https://registry.npmjs.org/zksync-web3/-/zksync-web3-0.14.3.tgz" - integrity sha512-hT72th4AnqyLW1d5Jlv8N2B/qhEnl2NePK2A3org7tAa24niem/UAaHMkEvmWI3SF9waYUPtqAtjpf+yvQ9zvQ== +zksync-ethers@^5.0.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/zksync-ethers/-/zksync-ethers-5.7.2.tgz#e965a9926e6f8168963ab565dd6ad0d38c4f7f18" + integrity sha512-D+wn4nkGixUOek9ZsVvIZ/MHponQ5xvw74FSbDJDv6SLCI4LZALOAc8lF3b1ml8nOkpeE2pGV0VKmHTSquRNJg== + dependencies: + ethers "~5.7.0" zod@^3.21.4: version "3.22.4"