Skip to content

Commit 01b85bb

Browse files
chore: fix ledger payload size signing - canary
1 parent 1b08208 commit 01b85bb

File tree

4 files changed

+257
-88
lines changed

4 files changed

+257
-88
lines changed

packages/wallets/wallet-ledger/src/strategy/Ledger/Base.ts

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ import {
2323
DEFAULT_NUM_ADDRESSES_TO_FETCH,
2424
} from '@injectivelabs/wallet-base'
2525
import LedgerHW from './hw/index.js'
26-
import { domainHash, messageHash } from './utils.js'
2726
import { LedgerEip1193Provider } from './Eip1193Provider.js'
27+
import {
28+
domainHash,
29+
messageHash,
30+
type EIP712Message,
31+
isEIP712PayloadTooBig,
32+
} from './utils.js'
2833
import type { Hash } from 'viem'
2934
import type { PublicClient } from 'viem'
3035
import type { EvmChainId } from '@injectivelabs/ts-types'
@@ -211,15 +216,19 @@ export default class LedgerBase
211216
const derivationPath = await this.getDerivationPath(address)
212217
const object = JSON.parse(eip712json)
213218

219+
if (isEIP712PayloadTooBig(object)) {
220+
console.log('Payload is too big, signing with hashed message')
221+
return this.signEIP712HashedMessage(derivationPath, object)
222+
}
223+
214224
try {
225+
console.log('Payload is not too big, signing with message')
215226
const ledger = await this.ledger.getInstance()
216227
const result = await ledger.signEIP712Message(derivationPath, object)
217228

218-
const v = result.v.toString(16).padStart(2, '0')
219-
const combined = `${result.r}${result.s}${v}`
220-
221-
return combined.startsWith('0x') ? combined : `0x${combined}`
229+
return this.formatSignatureResult(result)
222230
} catch (e: unknown) {
231+
console.log('Error signing EIP712 message:', e)
223232
const errorMessage = (e as any).message
224233
const isKnownNanoSError =
225234
errorMessage.includes('instruction not supported') ||
@@ -235,28 +244,43 @@ export default class LedgerBase
235244
})
236245
}
237246

238-
try {
239-
const ledger = await this.ledger.getInstance()
240-
const result = await ledger.signEIP712HashedMessage(
241-
derivationPath,
242-
domainHash(object),
243-
messageHash(object),
244-
)
247+
return this.signEIP712HashedMessage(derivationPath, object)
248+
}
249+
}
245250

246-
const v = result.v.toString(16).padStart(2, '0')
247-
const combined = `${result.r}${result.s}${v}`
251+
private async signEIP712HashedMessage(
252+
derivationPath: string,
253+
object: EIP712Message,
254+
): Promise<string> {
255+
try {
256+
const ledger = await this.ledger.getInstance()
257+
const result = await ledger.signEIP712HashedMessage(
258+
derivationPath,
259+
domainHash(object),
260+
messageHash(object),
261+
)
248262

249-
return combined.startsWith('0x') ? combined : `0x${combined}`
250-
} catch (e) {
251-
throw new LedgerException(new Error((e as any).message), {
252-
code: UnspecifiedErrorCode,
253-
type: ErrorType.WalletError,
254-
contextModule: WalletAction.SignTransaction,
255-
})
256-
}
263+
return this.formatSignatureResult(result)
264+
} catch (e) {
265+
throw new LedgerException(new Error((e as any).message), {
266+
code: UnspecifiedErrorCode,
267+
type: ErrorType.WalletError,
268+
contextModule: WalletAction.SignTransaction,
269+
})
257270
}
258271
}
259272

273+
private formatSignatureResult(result: {
274+
v: number
275+
r: string
276+
s: string
277+
}): string {
278+
const v = result.v.toString(16).padStart(2, '0')
279+
const combined = `${result.r}${result.s}${v}`
280+
281+
return combined.startsWith('0x') ? combined : `0x${combined}`
282+
}
283+
260284
async signAminoCosmosTransaction(_transaction: {
261285
address: string
262286
signDoc: StdSignDoc
@@ -299,10 +323,7 @@ export default class LedgerBase
299323
uint8ArrayToHex(stringToUint8Array(toUtf8(data))),
300324
)
301325

302-
const v = result.v.toString(16).padStart(2, '0')
303-
const combined = `${result.r}${result.s}${v}`
304-
305-
return combined.startsWith('0x') ? combined : `0x${combined}`
326+
return this.formatSignatureResult(result)
306327
} catch (e: unknown) {
307328
throw new LedgerException(new Error((e as any).message), {
308329
code: UnspecifiedErrorCode,

packages/wallets/wallet-ledger/src/strategy/Ledger/ledgerUtils.spec.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,104 @@
1-
import { domainHash, messageHash } from './utils.js'
1+
import { domainHash, messageHash, isEIP712PayloadTooBig } from './utils.js'
2+
3+
const tooBigPayload = {
4+
types: {
5+
EIP712Domain: [
6+
{
7+
name: 'name',
8+
type: 'string',
9+
},
10+
{
11+
name: 'version',
12+
type: 'string',
13+
},
14+
{
15+
name: 'chainId',
16+
type: 'uint256',
17+
},
18+
{
19+
name: 'verifyingContract',
20+
type: 'address',
21+
},
22+
{
23+
name: 'salt',
24+
type: 'string',
25+
},
26+
],
27+
Tx: [
28+
{
29+
name: 'context',
30+
type: 'string',
31+
},
32+
{
33+
name: 'msgs',
34+
type: 'string',
35+
},
36+
],
37+
},
38+
primaryType: 'Tx',
39+
domain: {
40+
name: 'Injective Web3',
41+
version: '1.0.0',
42+
chainId: '0x1',
43+
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
44+
salt: '0',
45+
},
46+
message: {
47+
context:
48+
'{"account_number":1607336,"chain_id":"injective-1","fee":{"amount":[{"denom":"inj","amount":"452096000000000"}],"gas":904192,"payer":"inj1065f86fh88ptyrg8h5048zu0vyx7ex8ymwgr6h"},"memo":"","sequence":8,"timeout_height":146288343}',
49+
msgs: '[{"@type":"/cosmos.authz.v1beta1.MsgGrant","granter":"inj1qa8jmnd0tql2uc7yachkyqc22dqnz2uxcs9ra6","grantee":"inj1hcsytnq2salprkkscr73fzj7y9p8srgpncjhqx","grant":{"authorization":{"@type":"/cosmos.authz.v1beta1.GenericAuthorization","msg":"/injective.exchange.v1beta1.MsgWithdraw"},"expiration":"2030-12-17T23:00:00Z"}},{"@type":"/cosmos.authz.v1beta1.MsgGrant","granter":"inj1qa8jmnd0tql2uc7yachkyqc22dqnz2uxcs9ra6","grantee":"inj1hcsytnq2salprkkscr73fzj7y9p8srgpncjhqx","grant":{"authorization":{"@type":"/cosmos.authz.v1beta1.GenericAuthorization","msg":"/injective.exchange.v1beta1.MsgBatchUpdateOrders"},"expiration":"2030-12-17T23:00:00Z"}},{"@type":"/cosmos.authz.v1beta1.MsgGrant","granter":"inj1qa8jmnd0tql2uc7yachkyqc22dqnz2uxcs9ra6","grantee":"inj1hcsytnq2salprkkscr73fzj7y9p8srgpncjhqx","grant":{"authorization":{"@type":"/cosmos.authz.v1beta1.GenericAuthorization","msg":"/injective.exchange.v1beta1.MsgCreateSpotMarketOrder"},"expiration":"2030-12-17T23:00:00Z"}},{"@type":"/injective.wasmx.v1.MsgExecuteContractCompat","sender":"inj1qa8jmnd0tql2uc7yachkyqc22dqnz2uxcs9ra6","contract":"inj1hcsytnq2salprkkscr73fzj7y9p8srgpncjhqx","msg":"{\\"create_strategy\\":{\\"bounds\\":[\\"4000000\\",\\"5000000\\"],\\"levels\\":3,\\"subaccount_id\\":\\"0x074f2dcdaf583eae63c4ee2f62030a5341312b860000696e6a2d757364742d70\\",\\"fee_recipient\\":\\"inj1jv65s3grqf6v6jl3dp4t6c9t9rk99cd8dkncm8\\",\\"slippage\\":\\"0.1\\",\\"strategy_type\\":{\\"perpetual\\":{\\"margin_ratio\\":\\"0.83\\"}}}}","funds":"10000000peggy0xdAC17F958D2ee523a2206206994597C13D831ec7"}]',
50+
},
51+
}
52+
53+
const validPayload = {
54+
types: {
55+
EIP712Domain: [
56+
{
57+
name: 'name',
58+
type: 'string',
59+
},
60+
{
61+
name: 'version',
62+
type: 'string',
63+
},
64+
{
65+
name: 'chainId',
66+
type: 'uint256',
67+
},
68+
{
69+
name: 'verifyingContract',
70+
type: 'address',
71+
},
72+
{
73+
name: 'salt',
74+
type: 'string',
75+
},
76+
],
77+
Tx: [
78+
{
79+
name: 'context',
80+
type: 'string',
81+
},
82+
{
83+
name: 'msgs',
84+
type: 'string',
85+
},
86+
],
87+
},
88+
primaryType: 'Tx',
89+
domain: {
90+
name: 'Injective Web3',
91+
version: '1.0.0',
92+
chainId: '0x1',
93+
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
94+
salt: '0',
95+
},
96+
message: {
97+
context:
98+
'{"account_number":1607336,"chain_id":"injective-1","fee":{"amount":[{"denom":"inj","amount":"89625000000000"}],"gas":179250,"payer":"inj1065f86fh88ptyrg8h5048zu0vyx7ex8ymwgr6h"},"memo":"","sequence":8,"timeout_height":146288649}',
99+
msgs: '[{"@type":"/injective.exchange.v1beta1.MsgCreateSpotLimitOrder","sender":"inj1qa8jmnd0tql2uc7yachkyqc22dqnz2uxcs9ra6","order":{"market_id":"0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0","order_info":{"subaccount_id":"0x074f2dcdaf583eae63c4ee2f62030a5341312b86000000000000000000000000","fee_recipient":"inj1jv65s3grqf6v6jl3dp4t6c9t9rk99cd8dkncm8","price":"0.000000000004601000","quantity":"434000000000000000.000000000000000000","cid":""},"order_type":"BUY","trigger_price":"0.000000000000000000"}}]',
100+
},
101+
}
2102

3103
const payload = {
4104
types: {
@@ -48,3 +148,15 @@ describe('Ledger Utils', () => {
48148
expect(expected).toMatch(messageHashOutput)
49149
})
50150
})
151+
152+
describe('isEIP712PayloadTooBig', () => {
153+
test('returns true if payload is too big', () => {
154+
const result = isEIP712PayloadTooBig(tooBigPayload)
155+
expect(result).toBe(true)
156+
})
157+
158+
test('returns false if payload is valid', () => {
159+
const result = isEIP712PayloadTooBig(validPayload)
160+
expect(result).toBe(false)
161+
})
162+
})

packages/wallets/wallet-ledger/src/strategy/Ledger/utils.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { hashDomain, hashStruct } from 'viem'
22

33
type TypeDefinition = Record<string, Array<{ name: string; type: string }>>
44

5-
interface EIP712Message {
5+
export interface EIP712Message {
66
types: TypeDefinition
77
domain: Record<string, any>
88
primaryType: string
@@ -24,3 +24,99 @@ export const messageHash = (message: EIP712Message): `0x${string}` => {
2424
primaryType: message.primaryType,
2525
})
2626
}
27+
28+
/**
29+
* Checks if an EIP-712 payload is too large for Ledger hardware wallet signing
30+
* @param {Object} eip712Payload - The EIP-712 typed data object
31+
* @returns {boolean} - Returns true if payload is too big, false otherwise
32+
*/
33+
export function isEIP712PayloadTooBig(eip712Payload: any) {
34+
// Ledger-specific limits (based on observed behavior and firmware constraints)
35+
const LIMITS = {
36+
// Maximum size of the entire JSON payload in bytes
37+
MAX_TOTAL_SIZE: 8000,
38+
39+
// Maximum size of the message.msgs field (often the largest field)
40+
MAX_MSGS_SIZE: 4000,
41+
42+
// Maximum size of the message.context field
43+
MAX_CONTEXT_SIZE: 2000,
44+
45+
// Maximum nesting depth for escaped JSON strings
46+
MAX_NESTING_DEPTH: 3,
47+
48+
// Maximum number of messages in a batch transaction
49+
MAX_MESSAGE_COUNT: 3,
50+
}
51+
52+
try {
53+
// Check 1: Total payload size
54+
const totalSize = JSON.stringify(eip712Payload).length
55+
if (totalSize > LIMITS.MAX_TOTAL_SIZE) {
56+
console.log(
57+
`❌ Total payload size (${totalSize} bytes) exceeds limit (${LIMITS.MAX_TOTAL_SIZE} bytes)`,
58+
)
59+
return true
60+
}
61+
62+
// Check 2: Message msgs field size
63+
if (eip712Payload.message?.msgs) {
64+
const msgsSize = eip712Payload.message.msgs.length
65+
if (msgsSize > LIMITS.MAX_MSGS_SIZE) {
66+
console.log(
67+
`❌ msgs field size (${msgsSize} bytes) exceeds limit (${LIMITS.MAX_MSGS_SIZE} bytes)`,
68+
)
69+
return true
70+
}
71+
72+
// Check 3: Count number of messages in the array
73+
try {
74+
const msgsArray = JSON.parse(eip712Payload.message.msgs)
75+
if (
76+
Array.isArray(msgsArray) &&
77+
msgsArray.length > LIMITS.MAX_MESSAGE_COUNT
78+
) {
79+
console.log(
80+
`❌ Message count (${msgsArray.length}) exceeds limit (${LIMITS.MAX_MESSAGE_COUNT})`,
81+
)
82+
return true
83+
}
84+
85+
// Check 4: Detect deeply nested escaped JSON (common in contract calls)
86+
for (const msg of msgsArray) {
87+
if (msg.msg && typeof msg.msg === 'string') {
88+
const escapeCount = (msg.msg.match(/\\\\/g) || []).length
89+
if (escapeCount > 10) {
90+
console.log(
91+
`❌ Detected deeply nested/escaped JSON (${escapeCount} escape sequences)`,
92+
)
93+
return true
94+
}
95+
}
96+
}
97+
} catch (e: any) {
98+
// If we can't parse msgs, it might be malformed
99+
console.warn('⚠️ Could not parse msgs field:', (e as any).message)
100+
}
101+
}
102+
103+
// Check 5: Context field size
104+
if (eip712Payload.message?.context) {
105+
const contextSize = eip712Payload.message.context.length
106+
if (contextSize > LIMITS.MAX_CONTEXT_SIZE) {
107+
console.log(
108+
`❌ context field size (${contextSize} bytes) exceeds limit (${LIMITS.MAX_CONTEXT_SIZE} bytes)`,
109+
)
110+
return true
111+
}
112+
}
113+
114+
// All checks passed
115+
// console.log('✅ Payload size is within acceptable limits')
116+
return false
117+
} catch (_error) {
118+
// console.error('Error validating payload:', _error)
119+
// If we can't validate, assume it might be too big
120+
return true
121+
}
122+
}

0 commit comments

Comments
 (0)