permissionless.js
permissionless.js copied to clipboard
Support Coinbase Smart Wallet
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
⚠️ 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
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!",
},
};
Legend
I've been waiting for this 🙏
plz merge asap
ty
whoa thank you @cstoneham !!!!! merge merge merge
@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 – 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!
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).
thanks @jxom, so fast! when do you think the next patch version might be released (2.17.6)?
Released on 2.17.7
thanks for your help @jxom @wilsoncusack, think this is ready for another review.
@kristofgazso thoughts on this?
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.
Hey I had a few question:
- What is the difference between ownerIndex & Index?
- If there are multiple
initialOwnerswill the privateKey used here still be able to sign and send user operation?
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.