Skip to content

Commit

Permalink
Merge pull request #4 from Moonsong-Labs/update-upstream
Browse files Browse the repository at this point in the history
chore: update upstream
  • Loading branch information
aon authored Jan 17, 2025
2 parents 14c454b + ed8c70c commit 9fd9cc8
Show file tree
Hide file tree
Showing 6 changed files with 563 additions and 726 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ jobs:
- name: Deploy Demo-App contracts
run: pnpm nx deploy-contracts demo-app

- name: Run tests
run: pnpm test
working-directory: packages/sdk

# Run E2E tests
- name: Install Playwright Chromium Browser
run: pnpm exec playwright install chromium
Expand Down
6 changes: 4 additions & 2 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/_types --emitDeclarationOnly --declaration --declarationMap",
"clean": "rm -rf *.tsbuildinfo dist",
"typecheck": "tsc --noEmit",
"publish:local": "pnpm publish --no-git-checks --force"
"publish:local": "pnpm publish --no-git-checks --force",
"test": "vitest"
},
"peerDependencies": {
"@simplewebauthn/browser": "10.x",
Expand All @@ -37,7 +38,8 @@
"@types/ms": "^0.7.34",
"@types/node": "^22.1.0",
"eventemitter3": "^5.0.1",
"viem": "2.21.14"
"viem": "2.21.14",
"vitest": "^2.1.8"
},
"files": [
"*",
Expand Down
228 changes: 228 additions & 0 deletions packages/sdk/src/client/passkey/actions/account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { type Address, type Hash, type TransactionReceipt } from "viem";
import { waitForTransactionReceipt, writeContract } from "viem/actions";
import { describe, expect, test, vi } from "vitest";

import { deployAccount } from "./account.js";

// Mock the passkey utils
vi.mock("../../../utils/passkey.js", () => ({
getPublicKeyBytesFromPasskeySignature: vi.fn().mockReturnValue([
Buffer.from("0000000000000000000000000000000000000000000000000000000000000001", "hex"),
Buffer.from("0000000000000000000000000000000000000000000000000000000000000002", "hex"),
]),
}));

// Mock viem actions
vi.mock("viem/actions", () => ({
writeContract: vi.fn(),
waitForTransactionReceipt: vi.fn(),
}));

// Add FactoryAbi mock at the top with other mocks
vi.mock("../../../abi/Factory.js", () => ({
FactoryAbi: [
{
inputs: [
{ type: "bytes32", name: "_salt" },
{ type: "string", name: "_uniqueAccountId" },
{ type: "bytes[]", name: "_initialValidators" },
{ type: "address[]", name: "_initialK1Owners" },
],
name: "deployProxySsoAccount",
outputs: [{ type: "address", name: "accountAddress" }],
stateMutability: "nonpayable",
type: "function",
},
],
}));

describe("deployAccount", () => {
// Setup common test data
const mockSalt = new Uint8Array([
213, 36, 52, 69, 251, 82, 199, 45, 113, 6, 20, 213, 78, 47, 165,
164, 106, 221, 105, 67, 247, 47, 200, 167, 137, 64, 151, 12, 179,
74, 90, 23,
]);

// CBOR-encoded COSE key with known x,y coordinates
const mockCredentialPublicKey = new Uint8Array([
0xa5, // map of 5 pairs
0x01, // key 1 (kty)
0x02, // value 2 (EC2)
0x03, // key 3 (alg)
0x26, // value -7 (ES256)
0x20, // key -1 (crv)
0x01, // value 1 (P-256)
0x21, // key -2 (x coordinate)
0x58, 0x20, // bytes(32)
...new Uint8Array(32).fill(0x01), // x coordinate filled with 0x01
0x22, // key -3 (y coordinate)
0x58, 0x20, // bytes(32)
...new Uint8Array(32).fill(0x02), // y coordinate filled with 0x02
]);

const mockClient = {
account: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
chain: { id: 1 },
} as any;
const mockContracts = {
accountFactory: "0x1234567890123456789012345678901234567890" as Address,
passkey: "0x2234567890123456789012345678901234567890" as Address,
session: "0x3234567890123456789012345678901234567890" as Address,
};

const mockTransactionHash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" as Hash;
const mockTransactionReceipt: TransactionReceipt = {
status: "success",
contractAddress: "0x4234567890123456789012345678901234567890",
blockNumber: 1n,
blockHash: "0x5e1d3a76f1b1c3a2b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7" as Hash,
transactionHash: mockTransactionHash,
logs: [],
logsBloom: "0x",
cumulativeGasUsed: 0n,
effectiveGasPrice: 0n,
gasUsed: 0n,
type: "eip1559",
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
to: "0x1234567890123456789012345678901234567890",
transactionIndex: 0,
};

test("deploys account successfully", async () => {
// Setup mocks
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);

const result = await deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
salt: mockSalt,
});

// Verify the result
expect(result).toEqual({
address: "0x4234567890123456789012345678901234567890",
transactionReceipt: mockTransactionReceipt,
});

// Verify writeContract was called with correct parameters
expect(writeContract).toHaveBeenCalledWith(
mockClient,
expect.objectContaining({
address: mockContracts.accountFactory,
functionName: "deployProxySsoAccount",
}),
);
});

test("handles transaction failure", async () => {
// Setup mock for failed transaction
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue({
...mockTransactionReceipt,
status: "reverted",
});

await expect(
deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
salt: mockSalt,
}),
).rejects.toThrow("Account deployment transaction reverted");
});

test("handles missing contract address in receipt", async () => {
// Setup mock for missing contract address
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue({
...mockTransactionReceipt,
contractAddress: null,
});

await expect(
deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
salt: mockSalt,
}),
).rejects.toThrow("No contract address in transaction receipt");
});

test("calls onTransactionSent callback when provided", async () => {
const onTransactionSent = vi.fn();
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);

await deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
salt: mockSalt,
onTransactionSent,
});

expect(onTransactionSent).toHaveBeenCalledWith(mockTransactionHash);
});

test("uses window.location.origin when expectedOrigin is not provided", async () => {
// Mock window.location
const originalWindow = global.window;
global.window = {
...originalWindow,
location: {
...originalWindow?.location,
origin: "https://example.com",
},
} as any;

vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);

const writeContractSpy = vi.mocked(writeContract);
await deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
salt: mockSalt,
});

// Simpler assertion that just checks the key parts
const lastCall = writeContractSpy.mock.lastCall;
expect(lastCall?.[0]).toBe(mockClient);
expect(lastCall?.[1]).toMatchObject({
address: mockContracts.accountFactory,
functionName: "deployProxySsoAccount",
});

// Restore window
global.window = originalWindow;
});

test("handles paymaster configuration", async () => {
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);

const paymasterAddress = "0x5234567890123456789012345678901234567890" as Address;
const paymasterInput = "0x1234" as const;

await deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
paymasterAddress,
paymasterInput,
});

expect(writeContract).toHaveBeenCalledWith(
mockClient,
expect.objectContaining({
paymaster: paymasterAddress,
paymasterInput,
}),
);
});
});
47 changes: 47 additions & 0 deletions packages/sdk/src/utils/encoding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, test } from "vitest";

import { encodeModuleData, encodePasskeyModuleParameters } from "./encoding";

describe("encoding utils", () => {
describe("encodePasskeyModuleParameters", () => {
test("correctly encodes passkey parameters", () => {
const passkey = {
passkeyPublicKey: [
Buffer.from("1234567890123456789012345678901234567890123456789012345678901234", "hex"),
Buffer.from("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd", "hex"),
],
expectedOrigin: "https://example.com",
};

const encoded = encodePasskeyModuleParameters(passkey);

// The encoding should be a hex string
expect(encoded).toMatch(/^0x[0-9a-f]+$/i);

// Should contain both public key components and the origin
expect(encoded).toContain("1234567890123456789012345678901234567890123456789012345678901234");
expect(encoded).toContain("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd");
expect(encoded).toContain(Buffer.from("https://example.com").toString("hex"));
expect(encoded).toEqual("0x1234567890123456789012345678901234567890123456789012345678901234abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001368747470733a2f2f6578616d706c652e636f6d00000000000000000000000000");
});
});

describe("encodeModuleData", () => {
test("correctly encodes module data", () => {
const moduleData = {
address: "0x1234567890123456789012345678901234567890" as const,
parameters: "0xabcdef" as const,
};

const encoded = encodeModuleData(moduleData);

// The encoding should be a hex string
expect(encoded).toMatch(/^0x[0-9a-f]+$/i);

// Should contain both the address and parameters
expect(encoded.toLowerCase()).toContain(moduleData.address.slice(2).toLowerCase());
expect(encoded.toLowerCase()).toContain(moduleData.parameters.slice(2).toLowerCase());
expect(encoded).toEqual("0x000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003abcdef0000000000000000000000000000000000000000000000000000000000");
});
});
});
88 changes: 88 additions & 0 deletions packages/sdk/src/utils/passkey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, test } from "vitest";

import {
getPasskeySignatureFromPublicKeyBytes,
getPublicKeyBytesFromPasskeySignature,
} from "./passkey";

describe("passkey utils", () => {
describe("getPublicKeyBytesFromPasskeySignature", () => {
test("correctly decodes CBOR-encoded COSE key", () => {
// This is a sample CBOR-encoded COSE key with known x,y coordinates
// Format: map with 5 entries:
// 1: 2 (kty: EC2)
// 3: -7 (alg: ES256)
// -1: 1 (crv: P-256)
// -2: x coordinate (32 bytes)
// -3: y coordinate (32 bytes)
const samplePublicKey = new Uint8Array([
0xa5, // map of 5 pairs
0x01, // key 1 (kty)
0x02, // value 2 (EC2)
0x03, // key 3 (alg)
0x26, // value -7 (ES256)
0x20, // key -1 (crv)
0x01, // value 1 (P-256)
0x21, // key -2 (x coordinate)
0x58,
0x20, // bytes(32)
...new Uint8Array(32).fill(0x01), // x coordinate filled with 0x01
0x22, // key -3 (y coordinate)
0x58,
0x20, // bytes(32)
...new Uint8Array(32).fill(0x02), // y coordinate filled with 0x02
]);

const [x, y] = getPublicKeyBytesFromPasskeySignature(samplePublicKey);

// Check that x coordinate is all 0x01
expect(Buffer.from(x).every((byte) => byte === 0x01)).toBe(true);
// Check that y coordinate is all 0x02
expect(Buffer.from(y).every((byte) => byte === 0x02)).toBe(true);
// Check lengths
expect(x.length).toBe(32);
expect(y.length).toBe(32);
});

test("roundtrip conversion works", () => {
// Create sample x,y coordinates as hex strings
const xHex = "0x" + "01".repeat(32);
const yHex = "0x" + "02".repeat(32);

// Convert to COSE format
const coseKey = getPasskeySignatureFromPublicKeyBytes([xHex, yHex]);

// Convert back to coordinates
const [x, y] = getPublicKeyBytesFromPasskeySignature(coseKey);

// Check that we got back our original values
expect(Buffer.from(x).toString("hex")).toBe(xHex.slice(2));
expect(Buffer.from(y).toString("hex")).toBe(yHex.slice(2));
});

test("throws on invalid CBOR data", () => {
const invalidCBOR = new Uint8Array([0xff, 0xff, 0xff]); // Invalid CBOR bytes

expect(() => {
getPublicKeyBytesFromPasskeySignature(invalidCBOR);
}).toThrow();
});

test("throws if x or y coordinates are missing", () => {
// CBOR map with only kty, alg, and crv (missing x,y)
const incompleteCOSE = new Uint8Array([
0xa3, // map of 3 pairs
0x01, // key 1 (kty)
0x02, // value 2 (EC2)
0x03, // key 3 (alg)
0x26, // value -7 (ES256)
0x20, // key -1 (crv)
0x01, // value 1 (P-256)
]);

expect(() => {
getPublicKeyBytesFromPasskeySignature(incompleteCOSE);
}).toThrow();
});
});
});
Loading

0 comments on commit 9fd9cc8

Please sign in to comment.