From 8cdfc17b5a256ec68268459b6539e066af299c2f Mon Sep 17 00:00:00 2001 From: impelcrypto Date: Thu, 21 May 2026 16:10:13 +0800 Subject: [PATCH 1/2] feat: added Strium support --- .../RpcMethods/method/signMessageMethods.ts | 13 ++++++ .../shortcut/signMessageShortcuts.ts | 11 +++++ packages/app-sdk/package.json | 9 +++- packages/app-sdk/src/browser-entry.ts | 2 + packages/app-sdk/src/index.ts | 4 ++ .../deterministic-sign/index.test.ts | 41 +++++++++++++++++ .../deterministic-sign/index.ts | 44 +++++++++++++++++++ .../app-sdk/src/sign/app-sdk/Signer.test.ts | 1 + packages/app-sdk/src/sign/app-sdk/Signer.ts | 1 + .../app-sdk/src/sign/app-sdk/utils.test.ts | 2 + packages/app-sdk/src/sign/app-sdk/utils.ts | 2 + 11 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.test.ts create mode 100644 packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.ts diff --git a/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts b/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts index 45399add..3f39be1d 100644 --- a/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts +++ b/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts @@ -77,12 +77,25 @@ const ethSignTypedDataV4: RpcRequestInput = { ], } +const walletDeterministicSign: RpcRequestInput = { + method: 'wallet_deterministicSign', + params: [ + { key: 'message', required: true }, + { key: 'address', required: true }, + ], + format: (data: Record) => [ + `0x${Buffer.from(data.message, 'utf8').toString('hex')}`, + data.address, + ], +} + export const signMessageMethods = [ ethSign, personalSign, ethSignTypedDataV1, ethSignTypedDataV3, ethSignTypedDataV4, + walletDeterministicSign, ] 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..e32f02a4 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 walletDeterministicSignShortcuts: ShortcutType[] = [ + { + key: EXAMPLE_MESSAGE, + data: { + message: EXAMPLE_MESSAGE, + 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_deterministicSign: walletDeterministicSignShortcuts, }) diff --git a/packages/app-sdk/package.json b/packages/app-sdk/package.json index 3c1acd1a..c4cd85cd 100644 --- a/packages/app-sdk/package.json +++ b/packages/app-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@startale/app-sdk", - "version": "1.3.1", + "version": "1.3.2", "description": "Superapp SDK", "keywords": [ "startale", @@ -32,6 +32,11 @@ "import": "./dist/interface/public-utilities/spend-permission/index.js", "require": "./dist/interface/public-utilities/spend-permission/index.js" }, + "./deterministic-sign": { + "types": "./dist/interface/public-utilities/deterministic-sign/index.d.ts", + "import": "./dist/interface/public-utilities/deterministic-sign/index.js", + "require": "./dist/interface/public-utilities/deterministic-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..a3aadd7e 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 { requestDeterministicSign } from './interface/public-utilities/deterministic-sign/index.js' +export type { RequestDeterministicSignType } from './interface/public-utilities/deterministic-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..8ae617d2 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' +// Deterministic sign interface exports +export { requestDeterministicSign } from './interface/public-utilities/deterministic-sign/index.js' +export type { RequestDeterministicSignType } from './interface/public-utilities/deterministic-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/deterministic-sign/index.test.ts b/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.test.ts new file mode 100644 index 00000000..6fc92949 --- /dev/null +++ b/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from 'vitest' + +import { ProviderInterface } from ':core/provider/interface.js' +import { requestDeterministicSign } from './index.js' + +describe('requestDeterministicSign', () => { + const address = '0x1234567890abcdef1234567890abcdef12345678' as const + + const makeProvider = (returnValue: string): ProviderInterface => + ({ + request: vi.fn().mockResolvedValue(returnValue), + }) as unknown as ProviderInterface + + it('forwards a string message hex-encoded to wallet_deterministicSign', async () => { + const provider = makeProvider('0xdeadbeef') + + const signature = await requestDeterministicSign({ + provider, + message: 'Authorize', + address, + }) + + expect(provider.request).toHaveBeenCalledWith({ + method: 'wallet_deterministicSign', + params: ['0x417574686f72697a65', address], + }) + expect(signature).toBe('0xdeadbeef') + }) + + it('passes through a hex-encoded message unchanged', async () => { + const provider = makeProvider('0xdeadbeef') + const hex = '0x417574686f72697a65' as const + + await requestDeterministicSign({ provider, message: hex, address }) + + expect(provider.request).toHaveBeenCalledWith({ + method: 'wallet_deterministicSign', + params: [hex, address], + }) + }) +}) diff --git a/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.ts b/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.ts new file mode 100644 index 00000000..1d8560d1 --- /dev/null +++ b/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.ts @@ -0,0 +1,44 @@ +import { type Address, type Hex, toHex } from 'viem' + +import { ProviderInterface } from ':core/provider/interface.js' + +export type RequestDeterministicSignType = { + provider: ProviderInterface + message: string | Hex + address: Address +} + +/** + * Requests a deterministic signature from the Startale wallet. + * + * Unlike `personal_sign`, which returns a Smart Account / ERC-1271 wrapped + * signature whose bytes vary across calls (the underlying WebAuthn/Passkey + * signer is non-deterministic by spec), this method asks the popup to return + * a raw secp256k1 signature produced with RFC 6979 deterministic ECDSA against + * a stable per-user secret. + * + * Same user + same message → byte-identical signature on every call and every + * device (so long as the popup-side stable secret is reproducible — see the + * Startale popup implementation for the seed-resolution strategy). + * + * Typical use case: deriving a long-lived "agent wallet" seed from the user's + * Startale identity without depending on a backend key store. + */ +const requestDeterministicSignFn = async ({ + provider, + message, + address, +}: RequestDeterministicSignType): Promise => { + const hexMessage = typeof message === 'string' && message.startsWith('0x') + ? (message as Hex) + : toHex(message) + + const signature = (await provider.request({ + method: 'wallet_deterministicSign', + params: [hexMessage, address], + })) as Hex + + return signature +} + +export const requestDeterministicSign = requestDeterministicSignFn 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 827b1933..98a56f53 100644 --- a/packages/app-sdk/src/sign/app-sdk/Signer.test.ts +++ b/packages/app-sdk/src/sign/app-sdk/Signer.test.ts @@ -448,6 +448,7 @@ describe('Signer', () => { 'eth_ecRecover', 'personal_sign', 'wallet_sign', + 'wallet_deterministicSign', '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 24f2d17c..030cf979 100644 --- a/packages/app-sdk/src/sign/app-sdk/Signer.ts +++ b/packages/app-sdk/src/sign/app-sdk/Signer.ts @@ -267,6 +267,7 @@ export class Signer { case 'eth_ecRecover': case 'personal_sign': case 'wallet_sign': + case 'wallet_deterministicSign': 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 ae28c6f6..3b2ec414 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_deterministicSign', ['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_deterministicSign', ['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 be49eea5..240ca7ef 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_deterministicSign': 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_deterministicSign': params[1] = sender break default: From d9a11895634bddf4b4fda75a30a2ba8ed8d23331 Mon Sep 17 00:00:00 2001 From: impelcrypto Date: Wed, 10 Jun 2026 20:25:54 +0800 Subject: [PATCH 2/2] feat: rename deterministicSign to signRawDigest; bump 1.3.3 --- .../RpcMethods/method/signMessageMethods.ts | 13 ++--- .../shortcut/signMessageShortcuts.ts | 8 +-- packages/app-sdk/package.json | 10 ++-- packages/app-sdk/src/browser-entry.ts | 4 +- packages/app-sdk/src/index.ts | 6 +- .../deterministic-sign/index.test.ts | 41 -------------- .../deterministic-sign/index.ts | 44 --------------- .../raw-digest-sign/index.test.ts | 55 +++++++++++++++++++ .../public-utilities/raw-digest-sign/index.ts | 46 ++++++++++++++++ .../app-sdk/src/sign/app-sdk/Signer.test.ts | 2 +- packages/app-sdk/src/sign/app-sdk/Signer.ts | 2 +- .../app-sdk/src/sign/app-sdk/utils.test.ts | 4 +- packages/app-sdk/src/sign/app-sdk/utils.ts | 4 +- 13 files changed, 126 insertions(+), 113 deletions(-) delete mode 100644 packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.test.ts delete mode 100644 packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.ts create mode 100644 packages/app-sdk/src/interface/public-utilities/raw-digest-sign/index.test.ts create mode 100644 packages/app-sdk/src/interface/public-utilities/raw-digest-sign/index.ts diff --git a/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts b/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts index 3f39be1d..0f983422 100644 --- a/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts +++ b/examples/testapp/src/components/RpcMethods/method/signMessageMethods.ts @@ -77,16 +77,13 @@ const ethSignTypedDataV4: RpcRequestInput = { ], } -const walletDeterministicSign: RpcRequestInput = { - method: 'wallet_deterministicSign', +const walletSignRawDigest: RpcRequestInput = { + method: 'wallet_signRawDigest', params: [ - { key: 'message', required: true }, + { key: 'digest', required: true }, { key: 'address', required: true }, ], - format: (data: Record) => [ - `0x${Buffer.from(data.message, 'utf8').toString('hex')}`, - data.address, - ], + format: (data: Record) => [data.digest, data.address], } export const signMessageMethods = [ @@ -95,7 +92,7 @@ export const signMessageMethods = [ ethSignTypedDataV1, ethSignTypedDataV3, ethSignTypedDataV4, - walletDeterministicSign, + 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 e32f02a4..ee6c812f 100644 --- a/examples/testapp/src/components/RpcMethods/shortcut/signMessageShortcuts.ts +++ b/examples/testapp/src/components/RpcMethods/shortcut/signMessageShortcuts.ts @@ -11,11 +11,11 @@ const personalSignShortcuts: ShortcutType[] = [ }, ] -const walletDeterministicSignShortcuts: ShortcutType[] = [ +const walletSignRawDigestShortcuts: ShortcutType[] = [ { - key: EXAMPLE_MESSAGE, + key: 'Example digest', data: { - message: EXAMPLE_MESSAGE, + digest: `0x${'ab'.repeat(32)}`, address: ADDR_TO_FILL, }, }, @@ -190,5 +190,5 @@ export const signMessageShortcutsMap = (chainId: number) => ({ eth_signTypedData_v1: ethSignTypedDataV1Shortcuts, eth_signTypedData_v3: ethSignTypedDataV3Shortcuts(chainId), eth_signTypedData_v4: ethSignTypedDataV4Shortcuts(chainId), - wallet_deterministicSign: walletDeterministicSignShortcuts, + wallet_signRawDigest: walletSignRawDigestShortcuts, }) diff --git a/packages/app-sdk/package.json b/packages/app-sdk/package.json index c4cd85cd..ffebb9b5 100644 --- a/packages/app-sdk/package.json +++ b/packages/app-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@startale/app-sdk", - "version": "1.3.2", + "version": "1.3.3", "description": "Superapp SDK", "keywords": [ "startale", @@ -32,10 +32,10 @@ "import": "./dist/interface/public-utilities/spend-permission/index.js", "require": "./dist/interface/public-utilities/spend-permission/index.js" }, - "./deterministic-sign": { - "types": "./dist/interface/public-utilities/deterministic-sign/index.d.ts", - "import": "./dist/interface/public-utilities/deterministic-sign/index.js", - "require": "./dist/interface/public-utilities/deterministic-sign/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", diff --git a/packages/app-sdk/src/browser-entry.ts b/packages/app-sdk/src/browser-entry.ts index a3aadd7e..f000555a 100644 --- a/packages/app-sdk/src/browser-entry.ts +++ b/packages/app-sdk/src/browser-entry.ts @@ -31,8 +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 { requestDeterministicSign } from './interface/public-utilities/deterministic-sign/index.js' -export type { RequestDeterministicSignType } from './interface/public-utilities/deterministic-sign/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 8ae617d2..a443f5cb 100644 --- a/packages/app-sdk/src/index.ts +++ b/packages/app-sdk/src/index.ts @@ -12,9 +12,9 @@ export { createStartaleAccountSDK } from './interface/builder/core/createStartal export { getCryptoKeyAccount, removeCryptoKey } from './kms/crypto-key/index.js' -// Deterministic sign interface exports -export { requestDeterministicSign } from './interface/public-utilities/deterministic-sign/index.js' -export type { RequestDeterministicSignType } from './interface/public-utilities/deterministic-sign/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' diff --git a/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.test.ts b/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.test.ts deleted file mode 100644 index 6fc92949..00000000 --- a/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' - -import { ProviderInterface } from ':core/provider/interface.js' -import { requestDeterministicSign } from './index.js' - -describe('requestDeterministicSign', () => { - const address = '0x1234567890abcdef1234567890abcdef12345678' as const - - const makeProvider = (returnValue: string): ProviderInterface => - ({ - request: vi.fn().mockResolvedValue(returnValue), - }) as unknown as ProviderInterface - - it('forwards a string message hex-encoded to wallet_deterministicSign', async () => { - const provider = makeProvider('0xdeadbeef') - - const signature = await requestDeterministicSign({ - provider, - message: 'Authorize', - address, - }) - - expect(provider.request).toHaveBeenCalledWith({ - method: 'wallet_deterministicSign', - params: ['0x417574686f72697a65', address], - }) - expect(signature).toBe('0xdeadbeef') - }) - - it('passes through a hex-encoded message unchanged', async () => { - const provider = makeProvider('0xdeadbeef') - const hex = '0x417574686f72697a65' as const - - await requestDeterministicSign({ provider, message: hex, address }) - - expect(provider.request).toHaveBeenCalledWith({ - method: 'wallet_deterministicSign', - params: [hex, address], - }) - }) -}) diff --git a/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.ts b/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.ts deleted file mode 100644 index 1d8560d1..00000000 --- a/packages/app-sdk/src/interface/public-utilities/deterministic-sign/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { type Address, type Hex, toHex } from 'viem' - -import { ProviderInterface } from ':core/provider/interface.js' - -export type RequestDeterministicSignType = { - provider: ProviderInterface - message: string | Hex - address: Address -} - -/** - * Requests a deterministic signature from the Startale wallet. - * - * Unlike `personal_sign`, which returns a Smart Account / ERC-1271 wrapped - * signature whose bytes vary across calls (the underlying WebAuthn/Passkey - * signer is non-deterministic by spec), this method asks the popup to return - * a raw secp256k1 signature produced with RFC 6979 deterministic ECDSA against - * a stable per-user secret. - * - * Same user + same message → byte-identical signature on every call and every - * device (so long as the popup-side stable secret is reproducible — see the - * Startale popup implementation for the seed-resolution strategy). - * - * Typical use case: deriving a long-lived "agent wallet" seed from the user's - * Startale identity without depending on a backend key store. - */ -const requestDeterministicSignFn = async ({ - provider, - message, - address, -}: RequestDeterministicSignType): Promise => { - const hexMessage = typeof message === 'string' && message.startsWith('0x') - ? (message as Hex) - : toHex(message) - - const signature = (await provider.request({ - method: 'wallet_deterministicSign', - params: [hexMessage, address], - })) as Hex - - return signature -} - -export const requestDeterministicSign = requestDeterministicSignFn 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 98a56f53..1830b0ed 100644 --- a/packages/app-sdk/src/sign/app-sdk/Signer.test.ts +++ b/packages/app-sdk/src/sign/app-sdk/Signer.test.ts @@ -448,7 +448,7 @@ describe('Signer', () => { 'eth_ecRecover', 'personal_sign', 'wallet_sign', - 'wallet_deterministicSign', + '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 030cf979..0f731ac0 100644 --- a/packages/app-sdk/src/sign/app-sdk/Signer.ts +++ b/packages/app-sdk/src/sign/app-sdk/Signer.ts @@ -267,7 +267,7 @@ export class Signer { case 'eth_ecRecover': case 'personal_sign': case 'wallet_sign': - case 'wallet_deterministicSign': + 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 3b2ec414..7f550ec8 100644 --- a/packages/app-sdk/src/sign/app-sdk/utils.test.ts +++ b/packages/app-sdk/src/sign/app-sdk/utils.test.ts @@ -34,7 +34,7 @@ describe('utils', () => { ['wallet_sendCalls', [{ from: sender }], sender], ['eth_signTypedData_v4', [sender, {}], sender], ['personal_sign', ['message', sender], sender], - ['wallet_deterministicSign', ['message', sender], sender], + ['wallet_signRawDigest', ['message', sender], sender], ])( 'should return the sender from the request for %s', (method, params, sender) => { @@ -51,7 +51,7 @@ describe('utils', () => { ['wallet_sendCalls', [{}], [{ from: '0x123' }]], ['eth_signTypedData_v4', [undefined, {}], ['0x123', {}]], ['personal_sign', ['hello', undefined], ['hello', '0x123']], - ['wallet_deterministicSign', ['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 240ca7ef..8ef5dd2b 100644 --- a/packages/app-sdk/src/sign/app-sdk/utils.ts +++ b/packages/app-sdk/src/sign/app-sdk/utils.ts @@ -40,7 +40,7 @@ export function getSenderFromRequest(request: RequestArguments) { } switch (request.method) { case 'personal_sign': - case 'wallet_deterministicSign': + case 'wallet_signRawDigest': return request.params[1] as Address case 'eth_signTypedData_v4': return request.params[0] as Address @@ -68,7 +68,7 @@ export function addSenderToRequest(request: RequestArguments, sender: Address) { params[0] = sender break case 'personal_sign': - case 'wallet_deterministicSign': + case 'wallet_signRawDigest': params[1] = sender break default: