Skip to content

Commit

Permalink
Implement personal_sign (#2095)
Browse files Browse the repository at this point in the history
* Add sign message

* Update packages/fcl-ethereum-provider/src/accounts/account-manager.ts

Co-authored-by: Jordan Ribbink <[email protected]>

* Push fix

* Remove comment

* Update packages/fcl-ethereum-provider/src/rpc/handlers/personal-sign.ts

Co-authored-by: Jordan Ribbink <[email protected]>

* Update packages/fcl-ethereum-provider/src/accounts/account-manager.ts

Co-authored-by: Jordan Ribbink <[email protected]>

* Update packages/fcl-ethereum-provider/src/rpc/handlers/personal-sign.ts

Co-authored-by: Jordan Ribbink <[email protected]>

* Check auth with coa address

* Add tests

* Fix params and prettier

* Run prettier

* Use RLP

* Change path

* hex array

* Fix RLP test

* Run prettier

---------

Co-authored-by: Chase Fleming <[email protected]>
Co-authored-by: Jordan Ribbink <[email protected]>
  • Loading branch information
3 people authored Jan 30, 2025
1 parent a9659aa commit 2fda71b
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 1 deletion.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/fcl-ethereum-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"dependencies": {
"@babel/runtime": "^7.25.7",
"@onflow/fcl": "1.13.4",
"@onflow/rlp": "^1.2.3",
"@walletconnect/jsonrpc-http-connection": "^1.0.8",
"@walletconnect/jsonrpc-provider": "^1.0.14"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {AccountManager} from "./account-manager"
import {mockUser} from "../__mocks__/fcl"
import * as fcl from "@onflow/fcl"
import * as rlp from "@onflow/rlp"
import {CurrentUser} from "@onflow/typedefs"

jest.mock("@onflow/fcl", () => {
Expand All @@ -14,6 +15,11 @@ jest.mock("@onflow/fcl", () => {
}
})

jest.mock("@onflow/rlp", () => ({
encode: jest.fn(),
Buffer: jest.requireActual("@onflow/rlp").Buffer,
}))

const mockFcl = jest.mocked(fcl)
const mockQuery = jest.mocked(fcl.query)

Expand Down Expand Up @@ -333,3 +339,82 @@ describe("send transaction", () => {
)
})
})

describe("signMessage", () => {
let accountManager: AccountManager
let user: jest.Mocked<typeof fcl.currentUser>

beforeEach(() => {
user = mockUser()
accountManager = new AccountManager(user)
})

afterEach(() => {
jest.clearAllMocks()
})

it("should throw an error if the COA address is not available", async () => {
accountManager["coaAddress"] = null

await expect(
accountManager.signMessage("Test message", "0x1234")
).rejects.toThrow(
"COA address is not available. User might not be authenticated."
)
})

it("should throw an error if the signer address does not match the COA address", async () => {
accountManager["coaAddress"] = "0xCOA1"

await expect(
accountManager.signMessage("Test message", "0xDIFFERENT")
).rejects.toThrow("Signer address does not match authenticated COA address")
})

it("should successfully sign a message and return an RLP-encoded proof", async () => {
accountManager["coaAddress"] = "0xCOA1"
const mockSignature = "0xabcdef1234567890"
const mockRlpEncoded = "f86a808683abcdef682f73746f726167652f65766d"

user.signUserMessage = jest
.fn()
.mockResolvedValue([{addr: "0xCOA1", keyId: 0, signature: mockSignature}])

jest.mocked(rlp.encode).mockReturnValue(Buffer.from(mockRlpEncoded, "hex"))

const proof = await accountManager.signMessage("Test message", "0xCOA1")

expect(proof).toBe(`0x${mockRlpEncoded}`)

expect(user.signUserMessage).toHaveBeenCalledWith("Test message")

expect(rlp.encode).toHaveBeenCalledWith([
[0],
expect.any(Buffer),
"/public/evm",
[mockSignature],
])
})

it("should throw an error if signUserMessage returns an empty array", async () => {
accountManager["coaAddress"] = "0xCOA1"

user.signUserMessage = jest.fn().mockResolvedValue([])

await expect(
accountManager.signMessage("Test message", "0xCOA1")
).rejects.toThrow("Failed to sign message")
})

it("should throw an error if signUserMessage fails", async () => {
accountManager["coaAddress"] = "0xCOA1"

user.signUserMessage = jest
.fn()
.mockRejectedValue(new Error("Signing failed"))

await expect(
accountManager.signMessage("Test message", "0xCOA1")
).rejects.toThrow("Signing failed")
})
})
45 changes: 44 additions & 1 deletion packages/fcl-ethereum-provider/src/accounts/account-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fcl from "@onflow/fcl"
import {CurrentUser} from "@onflow/typedefs"
import * as rlp from "@onflow/rlp"
import {CompositeSignature, CurrentUser} from "@onflow/typedefs"
import {
ContractType,
EVENT_IDENTIFIERS,
Expand All @@ -9,6 +10,7 @@ import {
FlowNetwork,
} from "../constants"
import {TransactionExecutedEvent} from "../types/events"
import {EthSignatureResponse} from "../types/eth"

export class AccountManager {
private user: typeof fcl.currentUser
Expand Down Expand Up @@ -194,4 +196,45 @@ export class AccountManager {

return evmTxHash
}

public async signMessage(
message: string,
from: string
): Promise<EthSignatureResponse> {
if (!this.coaAddress) {
throw new Error(
"COA address is not available. User might not be authenticated."
)
}

if (from.toLowerCase() !== this.coaAddress.toLowerCase()) {
throw new Error("Signer address does not match authenticated COA address")
}

try {
const response: CompositeSignature[] =
await this.user.signUserMessage(message)

if (!response || response.length === 0) {
throw new Error("Failed to sign message")
}

const keyIndices = response.map(sig => sig.keyId)
const signatures = response.map(sig => sig.signature)

const addressHexArray = Buffer.from(from.replace(/^0x/, ""), "hex")

const capabilityPath = "/public/evm"

const rlpEncodedProof = rlp
.encode([keyIndices, addressHexArray, capabilityPath, signatures])
.toString("hex")

return rlpEncodedProof.startsWith("0x")
? rlpEncodedProof
: `0x${rlpEncodedProof}` // Return 0x-prefix for Ethereum compatibility
} catch (error) {
throw error
}
}
}
11 changes: 11 additions & 0 deletions packages/fcl-ethereum-provider/src/rpc/handlers/personal-sign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {AccountManager} from "../../accounts/account-manager"
import {PersonalSignParams} from "../../types/eth"

export async function personalSign(
accountManager: AccountManager,
params: PersonalSignParams
) {
const [message, from] = params

return await accountManager.signMessage(message, from)
}
7 changes: 7 additions & 0 deletions packages/fcl-ethereum-provider/src/rpc/rpc-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {AccountManager} from "../accounts/account-manager"
import * as fcl from "@onflow/fcl"
import {FLOW_CHAINS, FlowNetwork} from "../constants"
import {ethSendTransaction} from "./handlers/eth-send-transaction"
import {personalSign} from "./handlers/personal-sign"
import {PersonalSignParams} from "../types/eth"

export class RpcProcessor {
constructor(
Expand All @@ -26,6 +28,11 @@ export class RpcProcessor {
return ethRequestAccounts(this.accountManager)
case "eth_sendTransaction":
return await ethSendTransaction(this.accountManager, params)
case "personal_sign":
return await personalSign(
this.accountManager,
params as PersonalSignParams
)
default:
return await this.gateway.request({
chainId: eip155ChainId,
Expand Down
3 changes: 3 additions & 0 deletions packages/fcl-ethereum-provider/src/types/eth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type EthSignatureResponse = string

export type PersonalSignParams = [string, string]

0 comments on commit 2fda71b

Please sign in to comment.