permissionless.js icon indicating copy to clipboard operation
permissionless.js copied to clipboard

Support Coinbase Smart Wallet

Open cstoneham opened this issue 1 year ago • 16 comments

Adds support for Coinbase Smart Wallet.

Working

  • Sending transactions

Not working

  • Signing Typed Data
  • Signing Messages

References

https://github.com/wilsoncusack/scw-tx https://github.com/coinbase/smart-wallet

cstoneham avatar Jul 09 '24 21:07 cstoneham

⚠️ No Changeset found

Latest commit: 2b0daa0d295efebedc05c12b5b725ab0fe71beba

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

changeset-bot[bot] avatar Jul 09 '24 21:07 changeset-bot[bot]

FYI this is the test suite that I have in our Rainmaker repo, I've just pulled it out as a snippet because I wasn't sure where this belongs in your codebase. Note that I have some test PKs that I pulled out (they'll need to be put back in), encoded data tests will have to change too.

import {
  CoinbaseSmartAccount,
  privateKeyToCoinbaseSmartAccount,
} from "common/lib/coinbase/privateKeyToCoinbaseSmartAccount";
import getPublicClient from "common/utils/getPublicClient";
import { ENTRYPOINT_ADDRESS_V06, isSmartAccountDeployed } from "permissionless";
import {
  ENTRYPOINT_ADDRESS_V06_TYPE,
  UserOperation,
} from "permissionless/types";
import { Address, Hex, PublicClient, TypedDataDefinition } from "viem";
import { privateKeyToAccount } from "viem/accounts";

describe("privateKeyToCoinbaseSmartAccount", () => {
  let publicClient: PublicClient;
  let deployedAccount: CoinbaseSmartAccount<ENTRYPOINT_ADDRESS_V06_TYPE>;
  let undeployedAccount: CoinbaseSmartAccount<ENTRYPOINT_ADDRESS_V06_TYPE>;

  beforeAll(async () => {
    publicClient = getPublicClient({
      chain: "base",
      type: "production",
    });

    const deployedPK =
      "0x";
    deployedAccount = await privateKeyToCoinbaseSmartAccount(publicClient, {
      privateKey: deployedPK,
      initialOwners: [privateKeyToAccount(deployedPK).address],
      factoryAddress: COINBASE_SMART_WALLET_FACTORY_ADDRESS,
      entryPoint: ENTRYPOINT_ADDRESS_V06,
      ownerIndex: 0n,
    });

    const undeployedPK =
      "0x";
    undeployedAccount = await privateKeyToCoinbaseSmartAccount(publicClient, {
      privateKey: undeployedPK,
      initialOwners: [privateKeyToAccount(undeployedPK).address],
      factoryAddress: COINBASE_SMART_WALLET_FACTORY_ADDRESS,
      entryPoint: ENTRYPOINT_ADDRESS_V06,
      ownerIndex: 1n,
    });
  });

  it("account addresses are different by pk", async () => {
    expect(deployedAccount.address).not.toBe(undeployedAccount.address);
  });

  it("account address - deployed", async () => {
    // verify deployment status
    const smartAccountDeployed = await isSmartAccountDeployed(
      publicClient,
      deployedAccount.address,
    );

    expect(smartAccountDeployed).toBe(true);
  });

  it("account address - undeployed", async () => {
    // verify deployment status
    const smartAccountDeployed = await isSmartAccountDeployed(
      publicClient,
      undeployedAccount.address,
    );

    expect(smartAccountDeployed).toBe(false);
  });

  it("signMessage - deployed", async () => {
    const message = "hello world";

    // sign the message
    const signedMessage = await deployedAccount.signMessage({
      message,
    });

    const result = await publicClient.verifyMessage({
      address: deployedAccount.address,
      message,
      signature: signedMessage,
    });

    expect(result).toBe(true);
  });

  it("signMessage - undeployed", async () => {
    const message = "hello world";

    // sign the message
    const signedMessage = await undeployedAccount.signMessage({
      message,
    });

    const result = await publicClient.verifyMessage({
      address: undeployedAccount.address,
      message,
      signature: signedMessage,
    });

    expect(result).toBe(true);
  });

  it("signTypedData - deployed", async () => {
    const signature = await deployedAccount.signTypedData(TYPED_DATA);

    const result = await publicClient.verifyTypedData({
      address: deployedAccount.address,
      domain: TYPED_DATA.domain,
      types: TYPED_DATA.types,
      primaryType: TYPED_DATA.primaryType,
      message: TYPED_DATA.message,
      signature,
    });

    expect(result).toBe(true);
  });

  it("signTypedData - undeployed", async () => {
    const signature = await undeployedAccount.signTypedData(TYPED_DATA);

    const result = await publicClient.verifyTypedData({
      address: undeployedAccount.address,
      domain: TYPED_DATA.domain,
      types: TYPED_DATA.types,
      primaryType: TYPED_DATA.primaryType,
      message: TYPED_DATA.message,
      signature,
    });

    expect(result).toBe(true);
  });

  it("getNonce - deployed", async () => {
    const nonce = await deployedAccount.getNonce();
    expect(nonce).toBeGreaterThanOrEqual(0);
  });

  it("getNonce - undeployed", async () => {
    const nonce = await undeployedAccount.getNonce();
    expect(nonce).eq(0n);
  });

  it("getInitCode - deployed", async () => {
    const initCode = await deployedAccount.getInitCode();
    expect(initCode).toBe("0x");
  });

  it("getInitCode - undeployed", async () => {
    const initCode = await undeployedAccount.getInitCode();
    expect(initCode).toBe(
      "0x4ada7b58d006aca9670daba300c0ee3f2ed1c1b03ffba36f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000D2c94f2c39de59ba7879d196d1fFc4c2D9ff7Fa",
    );
  });

  it("getFactory - deployed", async () => {
    const factory = await deployedAccount.getFactory();
    expect(factory).toBeUndefined();
  });

  it("getFactory - undeployed", async () => {
    const factory = await undeployedAccount.getFactory();
    expect(factory).toBe(RAINMAKER_WALLET_FACTORY_ADDRESS);
  });

  it("getFactoryData - deployed", async () => {
    const factoryData = await deployedAccount.getFactoryData();
    expect(factoryData).toBeUndefined();
  });

  it("getFactoryData - undeployed", async () => {
    const factoryData = await undeployedAccount.getFactoryData();
    expect(factoryData).toBe(
      "0x3ffba36f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000D2c94f2c39de59ba7879d196d1fFc4c2D9ff7Fa",
    );
  });

  it("encodeCallData", async () => {
    const sendCall = {
      to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" as Address,
      value: 1000000000000n,
      data: "0x" as Hex,
    };

    const encodedSingle = await undeployedAccount.encodeCallData(sendCall);

    expect(encodedSingle).toBe(
      "0xb61d27f6000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a5100000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000",
    );

    const encodedMulti = await undeployedAccount.encodeCallData([
      sendCall,
      sendCall,
    ]);

    expect(encodedMulti).toBe(
      "0x34fcd5be00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a5100000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a5100000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000",
    );
  });

  it("getDummySignature", async () => {
    const dummySignature = await undeployedAccount.getDummySignature(
      {} as UserOperation<"v0.6">,
    );

    expect(dummySignature).toBe(
      "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000041000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    );
  });
});

const TYPED_DATA: TypedDataDefinition = {
  domain: {
    name: "MyDapp",
    version: "1.0",
    chainId: 1,
    verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
  },
  types: {
    Person: [
      { name: "name", type: "string" },
      { name: "wallet", type: "address" },
    ],
    Mail: [
      { name: "from", type: "Person" },
      { name: "to", type: "Person" },
      { name: "contents", type: "string" },
    ],
  },
  primaryType: "Mail",
  message: {
    from: {
      name: "Alice",
      wallet: "0x1234567890123456789012345678901234567890",
    },
    to: {
      name: "Bob",
      wallet: "0x9876543210987654321098765432109876543210",
    },
    contents: "Hello, Bob!",
  },
};

cstoneham avatar Jul 10 '24 19:07 cstoneham

Legend

I've been waiting for this 🙏

plz merge asap

WardenJakx avatar Jul 10 '24 20:07 WardenJakx

ty

dqian avatar Jul 10 '24 20:07 dqian

whoa thank you @cstoneham !!!!! merge merge merge

carakessler avatar Jul 10 '24 20:07 carakessler

@wilsoncusack I notice that I'll sometimes get: Expected bytes32, got bytes31.5. depending on the message that I'm signing or the private key that I'm using, getting thrown from here:

export function buildSignatureWrapperForEOA({
  signature,
  ownerIndex,
}: {
  signature: SignReturnType;
  ownerIndex: bigint;
}): Hex {
  if (signature.v === undefined) {
    throw new Error("[buildSignatureWrapperForEOA] Invalid signature");
  }

  const signatureData = encodePacked(
    ["bytes32", "bytes32", "uint8"],
    [signature.r, signature.s, parseInt(signature.v.toString())],
  );
  return encodeAbiParameters(
    [SignatureWrapperStruct],
    [
      {
        ownerIndex,
        signatureData,
      },
    ],
  );
}

have you seen this before? I assume there's some hex padding or something that needs to be done.

more specifically the signature.r and/or signature.s will be 65 chars vs 66 which is why this is getting thrown.

cstoneham avatar Jul 18 '24 06:07 cstoneham

@cstoneham – that may be a small bug (or inconsistency rather) in Viem. Can you try [email protected] and tell me if that works? Can release shortly!

jxom avatar Jul 18 '24 07:07 jxom

[email protected]

Confirmed that this version fixes the issue, thanks @jxom.

What was the problem?

cstoneham avatar Jul 18 '24 16:07 cstoneham

Released! We needed to pad the signature properties to 32 bytes to conform with bytes32 encoding (31 bytes was still valid, but obviously doesn't fit well with ABI encoding APIs as you experienced).

jxom avatar Jul 18 '24 20:07 jxom

thanks @jxom, so fast! when do you think the next patch version might be released (2.17.6)?

cstoneham avatar Jul 19 '24 21:07 cstoneham

Released on 2.17.7

jxom avatar Jul 20 '24 11:07 jxom

thanks for your help @jxom @wilsoncusack, think this is ready for another review.

cstoneham avatar Jul 24 '24 15:07 cstoneham

@kristofgazso thoughts on this?

wilsoncusack avatar Aug 20 '24 18:08 wilsoncusack

Thank you @cstoneham let me check this today. I will be updating this branch and merging this with https://github.com/pimlicolabs/permissionless.js/pull/265 so that it's part of the 0.2 release.

plusminushalf avatar Aug 22 '24 15:08 plusminushalf

Hey I had a few question:

  1. What is the difference between ownerIndex & Index?
  2. If there are multiple initialOwners will the privateKey used here still be able to sign and send user operation?

plusminushalf avatar Aug 22 '24 16:08 plusminushalf

Also in v0.2 we are using Viem's account-abstraction methods and viem already had coin base implementation at toCoinbaseSmartAccount I am not sure permissionless also need to host the implementation again.

plusminushalf avatar Aug 22 '24 16:08 plusminushalf