diff --git a/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts b/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts index 45399add..0f983422 100644 --- a/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts +++ b/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts @@ -77,12 +77,22 @@ const ethSignTypedDataV4: RpcRequestInput = { ], } +const walletSignRawDigest: RpcRequestInput = { + method: 'wallet_signRawDigest', + params: [ + { key: 'digest', required: true }, + { key: 'address', required: true }, + ], + format: (data: Record) => [data.digest, data.address], +} + export const signMessageMethods = [ ethSign, personalSign, ethSignTypedDataV1, ethSignTypedDataV3, ethSignTypedDataV4, + walletSignRawDigest, ] export const verifySignMsg = async ({ diff --git a/examples/testapp/src/components/RpcMethods/shortcut/signMessageShortcuts.ts b/examples/testapp/src/components/RpcMethods/shortcut/signMessageShortcuts.ts index 5c40d2d1..ee6c812f 100644 --- a/examples/testapp/src/components/RpcMethods/shortcut/signMessageShortcuts.ts +++ b/examples/testapp/src/components/RpcMethods/shortcut/signMessageShortcuts.ts @@ -11,6 +11,16 @@ const personalSignShortcuts: ShortcutType[] = [ }, ] +const walletSignRawDigestShortcuts: ShortcutType[] = [ + { + key: 'Example digest', + data: { + digest: `0x${'ab'.repeat(32)}`, + address: ADDR_TO_FILL, + }, + }, +] + const ethSignTypedDataV1Shortcuts: ShortcutType[] = [ { key: EXAMPLE_MESSAGE, @@ -180,4 +190,5 @@ export const signMessageShortcutsMap = (chainId: number) => ({ eth_signTypedData_v1: ethSignTypedDataV1Shortcuts, eth_signTypedData_v3: ethSignTypedDataV3Shortcuts(chainId), eth_signTypedData_v4: ethSignTypedDataV4Shortcuts(chainId), + wallet_signRawDigest: walletSignRawDigestShortcuts, }) diff --git a/packages/app-sdk/package.json b/packages/app-sdk/package.json index 76469267..f666d705 100644 --- a/packages/app-sdk/package.json +++ b/packages/app-sdk/package.json @@ -32,6 +32,11 @@ "import": "./dist/interface/public-utilities/spend-permission/index.js", "require": "./dist/interface/public-utilities/spend-permission/index.js" }, + "./raw-digest-sign": { + "types": "./dist/interface/public-utilities/raw-digest-sign/index.d.ts", + "import": "./dist/interface/public-utilities/raw-digest-sign/index.js", + "require": "./dist/interface/public-utilities/raw-digest-sign/index.js" + }, "./ui-assets": { "types": "./dist/ui/assets/index.d.ts", "import": "./dist/ui/assets/index.js", @@ -117,4 +122,4 @@ "import": "*" } ] -} +} \ No newline at end of file diff --git a/packages/app-sdk/src/browser-entry.ts b/packages/app-sdk/src/browser-entry.ts index 685d787a..f000555a 100644 --- a/packages/app-sdk/src/browser-entry.ts +++ b/packages/app-sdk/src/browser-entry.ts @@ -31,6 +31,8 @@ export type { } from ':core/provider/interface.js' export { createStartaleAccountSDK } from './interface/builder/core/createStartaleAccountSDK.js' export { getCryptoKeyAccount, removeCryptoKey } from './kms/crypto-key/index.js' +export { requestSignRawDigest } from './interface/public-utilities/raw-digest-sign/index.js' +export type { RequestSignRawDigestType } from './interface/public-utilities/raw-digest-sign/index.js' export { base, CHAIN_IDS, getPaymentStatus, pay, TOKENS } export type { InfoRequest, diff --git a/packages/app-sdk/src/index.ts b/packages/app-sdk/src/index.ts index 97f63987..a443f5cb 100644 --- a/packages/app-sdk/src/index.ts +++ b/packages/app-sdk/src/index.ts @@ -12,6 +12,10 @@ export { createStartaleAccountSDK } from './interface/builder/core/createStartal export { getCryptoKeyAccount, removeCryptoKey } from './kms/crypto-key/index.js' +// Raw digest sign interface exports +export { requestSignRawDigest } from './interface/public-utilities/raw-digest-sign/index.js' +export type { RequestSignRawDigestType } from './interface/public-utilities/raw-digest-sign/index.js' + // Payment interface exports export { base, getPaymentStatus, pay } from './interface/payment/index.js' export type { diff --git a/packages/app-sdk/src/interface/public-utilities/raw-digest-sign/index.test.ts b/packages/app-sdk/src/interface/public-utilities/raw-digest-sign/index.test.ts new file mode 100644 index 00000000..93e3c073 --- /dev/null +++ b/packages/app-sdk/src/interface/public-utilities/raw-digest-sign/index.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it, vi } from 'vitest' + +import { ProviderInterface } from ':core/provider/interface.js' +import { requestSignRawDigest } from './index.js' + +describe('requestSignRawDigest', () => { + const address = '0x1234567890abcdef1234567890abcdef12345678' as const + const digest = `0x${'ab'.repeat(32)}` as const + + const makeProvider = (returnValue: string): ProviderInterface => + ({ + request: vi.fn().mockResolvedValue(returnValue), + }) as unknown as ProviderInterface + + it('forwards a 32-byte digest to wallet_signRawDigest', async () => { + const provider = makeProvider('0xdeadbeef') + + const signature = await requestSignRawDigest({ + provider, + digest, + address, + }) + + expect(provider.request).toHaveBeenCalledWith({ + method: 'wallet_signRawDigest', + params: [digest, address], + }) + expect(signature).toBe('0xdeadbeef') + }) + + it('rejects digests that are not exactly 32 bytes', async () => { + const provider = makeProvider('0xdeadbeef') + + await expect( + requestSignRawDigest({ + provider, + digest: '0x1234' as `0x${string}`, + address, + }), + ).rejects.toThrow('digest must be a 32-byte hex string') + expect(provider.request).not.toHaveBeenCalled() + }) + + it('rejects non-hex digests of the right length', async () => { + const provider = makeProvider('0xdeadbeef') + + await expect( + requestSignRawDigest({ + provider, + digest: `0x${'zz'.repeat(32)}` as `0x${string}`, + address, + }), + ).rejects.toThrow('digest must be a 32-byte hex string') + }) +}) diff --git a/packages/app-sdk/src/interface/public-utilities/raw-digest-sign/index.ts b/packages/app-sdk/src/interface/public-utilities/raw-digest-sign/index.ts new file mode 100644 index 00000000..99f49996 --- /dev/null +++ b/packages/app-sdk/src/interface/public-utilities/raw-digest-sign/index.ts @@ -0,0 +1,46 @@ +import { type Address, type Hex } from 'viem' + +import { ProviderInterface } from ':core/provider/interface.js' + +export type RequestSignRawDigestType = { + provider: ProviderInterface + /** 32-byte signing digest (0x + 64 hex chars) */ + digest: Hex + address: Address +} + +const DIGEST_PATTERN = /^0x[0-9a-fA-F]{64}$/ + +/** + * Asks the popup to raw-sign a 32-byte digest with the user's embedded + * wallet (EOA) — plain secp256k1 ECDSA over the exact digest bytes, with no + * EIP-191 prefix and no re-hashing. + * + * The signature bytes vary across calls (MPC signing is non-deterministic), + * but `ecrecover(digest, signature)` always yields the same signer address. + * Callers should rely on the recovered key, never on signature stability. + * + * Typical use cases: recovering a stable user address via ecrecover, and + * signing externally-computed digests such as Substrate extrinsic payloads + * for ethereum-type (AccountId20) chains. + */ +const requestSignRawDigestFn = async ({ + provider, + digest, + address, +}: RequestSignRawDigestType): Promise => { + if (!DIGEST_PATTERN.test(digest)) { + throw new Error( + 'digest must be a 32-byte hex string (0x followed by 64 hex chars)', + ) + } + + const signature = (await provider.request({ + method: 'wallet_signRawDigest', + params: [digest, address], + })) as Hex + + return signature +} + +export const requestSignRawDigest = requestSignRawDigestFn diff --git a/packages/app-sdk/src/sign/app-sdk/Signer.test.ts b/packages/app-sdk/src/sign/app-sdk/Signer.test.ts index c09f057e..0c8c1175 100644 --- a/packages/app-sdk/src/sign/app-sdk/Signer.test.ts +++ b/packages/app-sdk/src/sign/app-sdk/Signer.test.ts @@ -449,6 +449,7 @@ describe('Signer', () => { 'eth_ecRecover', 'personal_sign', 'wallet_sign', + 'wallet_signRawDigest', 'personal_ecRecover', 'eth_signTransaction', 'eth_sendTransaction', diff --git a/packages/app-sdk/src/sign/app-sdk/Signer.ts b/packages/app-sdk/src/sign/app-sdk/Signer.ts index 00728fe9..794360ee 100644 --- a/packages/app-sdk/src/sign/app-sdk/Signer.ts +++ b/packages/app-sdk/src/sign/app-sdk/Signer.ts @@ -269,6 +269,7 @@ export class Signer { case 'eth_ecRecover': case 'personal_sign': case 'wallet_sign': + case 'wallet_signRawDigest': case 'personal_ecRecover': case 'eth_signTransaction': case 'eth_sendTransaction': diff --git a/packages/app-sdk/src/sign/app-sdk/utils.test.ts b/packages/app-sdk/src/sign/app-sdk/utils.test.ts index f7eccde3..4882e4d1 100644 --- a/packages/app-sdk/src/sign/app-sdk/utils.test.ts +++ b/packages/app-sdk/src/sign/app-sdk/utils.test.ts @@ -34,6 +34,7 @@ describe('utils', () => { ['wallet_sendCalls', [{ from: sender }], sender], ['eth_signTypedData_v4', [sender, {}], sender], ['personal_sign', ['message', sender], sender], + ['wallet_signRawDigest', ['message', sender], sender], ])( 'should return the sender from the request for %s', (method, params, sender) => { @@ -50,6 +51,7 @@ describe('utils', () => { ['wallet_sendCalls', [{}], [{ from: '0x123' }]], ['eth_signTypedData_v4', [undefined, {}], ['0x123', {}]], ['personal_sign', ['hello', undefined], ['hello', '0x123']], + ['wallet_signRawDigest', ['hello', undefined], ['hello', '0x123']], ])( 'should enhance the request params for %s', (method, params, expectedParams) => { diff --git a/packages/app-sdk/src/sign/app-sdk/utils.ts b/packages/app-sdk/src/sign/app-sdk/utils.ts index 3b8716f6..7924603a 100644 --- a/packages/app-sdk/src/sign/app-sdk/utils.ts +++ b/packages/app-sdk/src/sign/app-sdk/utils.ts @@ -40,6 +40,7 @@ export function getSenderFromRequest(request: RequestArguments) { } switch (request.method) { case 'personal_sign': + case 'wallet_signRawDigest': return request.params[1] as Address case 'eth_signTypedData_v4': return request.params[0] as Address @@ -67,6 +68,7 @@ export function addSenderToRequest(request: RequestArguments, sender: Address) { params[0] = sender break case 'personal_sign': + case 'wallet_signRawDigest': params[1] = sender break default: