Skip to content

Commit 571eb93

Browse files
feat: implement EIP712 enc/decoding infrastructure (evmos#63)
* feat: implement sign doc to eip712 encoding infrastructure Add infrastructure to decode Amino/Protobuf SignDocs into EIP-712 objects. * fix: update global jest config Remove moduleDirectories to allow recursive imports within node_modules. * fix: remove babel dependency * fix: code cleanup and comments * fix: cover blank feePayer failure case in Amino SignDoc * fix: add blank feePayer test for Amino * fix: update chain_id regex matching * chore: remove jest from dependencies * fix: using evmos lib instead of tharsis * fix: export new encoding functions Co-authored-by: hanchon <[email protected]>
1 parent 75cc699 commit 571eb93

12 files changed

+959
-5
lines changed

jest.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ const { compilerOptions } = require('./tsconfig')
44
module.exports = {
55
// A list of reporter names that Jest uses when writing coverage reports
66
coverageReporters: ['json', 'html'],
7-
// An array of directory names to be searched recursively up from the requiring module's location
8-
moduleDirectories: ['node_modules', '<rootDir>', 'src'],
97
// An array of file extensions your modules use
108
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
119

packages/eip712/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@
2626
"start": "node dist/index.js"
2727
},
2828
"dependencies": {
29+
"@cosmjs/proto-signing": "^0.28.13",
30+
"@metamask/eth-sig-util": "^4.0.1",
31+
"@evmos/proto": "^0.1.21",
32+
"cosmjs-types": "^0.5.1",
2933
"link-module-alias": "^1.2.0",
34+
"long": "^5.2.0",
3035
"shx": "^0.3.4"
3136
},
3237
"gitHead": "fc2045b9357bde146e3374429453eb5d4a48a2ca",
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { generateTypes } from '../messages/base'
2+
import { MSG_VOTE_TYPES } from '../messages/gov'
3+
import { MSG_SEND_TYPES } from '../messages/msgsend'
4+
import { MSG_DELEGATE_TYPES } from '../messages/staking'
5+
import { decodeAminoSignDoc } from './decodeAmino'
6+
7+
// Testing Constants
8+
const eip712Domain = {
9+
name: 'Cosmos Web3',
10+
version: '1.0.0',
11+
chainId: 9000,
12+
verifyingContract: 'cosmos',
13+
salt: '0',
14+
}
15+
const eip712PrimaryType = 'Tx'
16+
17+
describe('decoding amino', () => {
18+
it('decodes msg_send payloads', () => {
19+
const byteString =
20+
'123 34 97 99 99 111 117 110 116 95 110 117 109 98 101 114 34 58 34 48 34 44 34 99 104 97 105 110 95 105 100 34 58 34 101 118 109 111 115 95 57 48 48 48 45 49 34 44 34 102 101 101 34 58 123 34 97 109 111 117 110 116 34 58 91 123 34 97 109 111 117 110 116 34 58 34 50 48 48 34 44 34 100 101 110 111 109 34 58 34 97 101 118 109 111 115 34 125 93 44 34 103 97 115 34 58 34 50 48 48 48 48 48 34 125 44 34 109 101 109 111 34 58 34 34 44 34 109 115 103 115 34 58 91 123 34 116 121 112 101 34 58 34 99 111 115 109 111 115 45 115 100 107 47 77 115 103 83 101 110 100 34 44 34 118 97 108 117 101 34 58 123 34 97 109 111 117 110 116 34 58 91 123 34 97 109 111 117 110 116 34 58 34 49 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 34 44 34 100 101 110 111 109 34 58 34 97 101 118 109 111 115 34 125 93 44 34 102 114 111 109 95 97 100 100 114 101 115 115 34 58 34 101 118 109 111 115 49 119 115 97 117 114 112 121 55 117 120 109 50 110 56 118 102 103 103 99 57 101 104 112 106 108 122 109 102 115 115 120 51 48 53 97 119 120 120 34 44 34 116 111 95 97 100 100 114 101 115 115 34 58 34 101 118 109 111 115 49 104 110 109 114 100 114 48 106 99 50 118 101 51 121 99 120 102 116 48 103 99 106 106 116 114 100 107 110 99 112 109 109 107 101 97 109 102 57 34 125 125 93 44 34 115 101 113 117 101 110 99 101 34 58 34 49 34 125'
21+
const bytes = Uint8Array.from(byteString.split(' ').map((el) => Number(el)))
22+
const eip712 = decodeAminoSignDoc(bytes)
23+
24+
expect(eip712.domain).toStrictEqual(eip712Domain)
25+
expect(eip712.primaryType).toBe(eip712PrimaryType)
26+
expect(eip712.types).toStrictEqual(generateTypes(MSG_SEND_TYPES))
27+
expect(eip712.message).toStrictEqual({
28+
account_number: '0',
29+
chain_id: 'evmos_9000-1',
30+
fee: {
31+
amount: [
32+
{
33+
amount: '200',
34+
denom: 'aevmos',
35+
},
36+
],
37+
gas: '200000',
38+
feePayer: 'evmos1wsaurpy7uxm2n8vfggc9ehpjlzmfssx305awxx',
39+
},
40+
memo: '',
41+
msgs: [
42+
{
43+
type: 'cosmos-sdk/MsgSend',
44+
value: {
45+
from_address: 'evmos1wsaurpy7uxm2n8vfggc9ehpjlzmfssx305awxx',
46+
to_address: 'evmos1hnmrdr0jc2ve3ycxft0gcjjtrdkncpmmkeamf9',
47+
amount: [
48+
{
49+
amount: '100000000000000000',
50+
denom: 'aevmos',
51+
},
52+
],
53+
},
54+
},
55+
],
56+
sequence: '1',
57+
})
58+
})
59+
60+
it('decodes msg_vote payloads', () => {
61+
const byteString =
62+
'123 34 97 99 99 111 117 110 116 95 110 117 109 98 101 114 34 58 34 48 34 44 34 99 104 97 105 110 95 105 100 34 58 34 101 118 109 111 115 95 57 48 48 48 45 49 34 44 34 102 101 101 34 58 123 34 97 109 111 117 110 116 34 58 91 123 34 97 109 111 117 110 116 34 58 34 50 48 48 48 34 44 34 100 101 110 111 109 34 58 34 97 101 118 109 111 115 34 125 93 44 34 103 97 115 34 58 34 50 48 48 48 48 48 34 125 44 34 109 101 109 111 34 58 34 34 44 34 109 115 103 115 34 58 91 123 34 116 121 112 101 34 58 34 99 111 115 109 111 115 45 115 100 107 47 77 115 103 86 111 116 101 34 44 34 118 97 108 117 101 34 58 123 34 111 112 116 105 111 110 34 58 49 44 34 112 114 111 112 111 115 97 108 95 105 100 34 58 34 49 34 44 34 118 111 116 101 114 34 58 34 101 118 109 111 115 49 121 103 120 113 50 53 118 108 112 51 117 52 108 113 121 121 115 54 118 114 115 100 97 122 57 119 119 57 107 103 114 120 55 120 108 104 116 121 34 125 125 93 44 34 115 101 113 117 101 110 99 101 34 58 34 49 34 125'
63+
const bytes = Uint8Array.from(byteString.split(' ').map((el) => Number(el)))
64+
const eip712 = decodeAminoSignDoc(bytes)
65+
66+
expect(eip712.domain).toStrictEqual(eip712Domain)
67+
expect(eip712.primaryType).toBe(eip712PrimaryType)
68+
expect(eip712.types).toStrictEqual(generateTypes(MSG_VOTE_TYPES))
69+
expect(eip712.message).toStrictEqual({
70+
account_number: '0',
71+
chain_id: 'evmos_9000-1',
72+
fee: {
73+
amount: [
74+
{
75+
amount: '2000',
76+
denom: 'aevmos',
77+
},
78+
],
79+
gas: '200000',
80+
feePayer: 'evmos1ygxq25vlp3u4lqyys6vrsdaz9ww9kgrx7xlhty',
81+
},
82+
memo: '',
83+
msgs: [
84+
{
85+
type: 'cosmos-sdk/MsgVote',
86+
value: {
87+
option: 1,
88+
proposal_id: '1',
89+
voter: 'evmos1ygxq25vlp3u4lqyys6vrsdaz9ww9kgrx7xlhty',
90+
},
91+
},
92+
],
93+
sequence: '1',
94+
})
95+
})
96+
97+
it('decodes msg_delegate payloads', () => {
98+
const byteString =
99+
'123 34 97 99 99 111 117 110 116 95 110 117 109 98 101 114 34 58 34 48 34 44 34 99 104 97 105 110 95 105 100 34 58 34 101 118 109 111 115 95 57 48 48 48 45 49 34 44 34 102 101 101 34 58 123 34 97 109 111 117 110 116 34 58 91 123 34 97 109 111 117 110 116 34 58 34 50 48 48 48 34 44 34 100 101 110 111 109 34 58 34 97 101 118 109 111 115 34 125 93 44 34 103 97 115 34 58 34 50 48 48 48 48 48 34 125 44 34 109 101 109 111 34 58 34 34 44 34 109 115 103 115 34 58 91 123 34 116 121 112 101 34 58 34 99 111 115 109 111 115 45 115 100 107 47 77 115 103 68 101 108 101 103 97 116 101 34 44 34 118 97 108 117 101 34 58 123 34 97 109 111 117 110 116 34 58 123 34 97 109 111 117 110 116 34 58 34 49 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 34 44 34 100 101 110 111 109 34 58 34 97 101 118 109 111 115 34 125 44 34 100 101 108 101 103 97 116 111 114 95 97 100 100 114 101 115 115 34 58 34 101 118 109 111 115 49 115 110 54 53 97 99 118 50 54 106 103 56 107 122 97 104 108 102 56 103 113 55 99 108 56 112 121 122 51 113 99 112 115 48 55 101 101 109 34 44 34 118 97 108 105 100 97 116 111 114 95 97 100 100 114 101 115 115 34 58 34 101 118 109 111 115 118 97 108 111 112 101 114 49 115 110 54 53 97 99 118 50 54 106 103 56 107 122 97 104 108 102 56 103 113 55 99 108 56 112 121 122 51 113 99 112 97 112 51 102 99 120 34 125 125 93 44 34 115 101 113 117 101 110 99 101 34 58 34 49 34 125'
100+
const bytes = Uint8Array.from(byteString.split(' ').map((el) => Number(el)))
101+
const eip712 = decodeAminoSignDoc(bytes)
102+
103+
expect(eip712.domain).toStrictEqual(eip712Domain)
104+
expect(eip712.primaryType).toBe(eip712PrimaryType)
105+
expect(eip712.types).toStrictEqual(generateTypes(MSG_DELEGATE_TYPES))
106+
expect(eip712.message).toStrictEqual({
107+
account_number: '0',
108+
chain_id: 'evmos_9000-1',
109+
fee: {
110+
amount: [
111+
{
112+
amount: '2000',
113+
denom: 'aevmos',
114+
},
115+
],
116+
gas: '200000',
117+
feePayer: 'evmos1sn65acv26jg8kzahlf8gq7cl8pyz3qcps07eem',
118+
},
119+
memo: '',
120+
msgs: [
121+
{
122+
type: 'cosmos-sdk/MsgDelegate',
123+
value: {
124+
amount: { amount: '1000000000000000000', denom: 'aevmos' },
125+
delegator_address: 'evmos1sn65acv26jg8kzahlf8gq7cl8pyz3qcps07eem',
126+
validator_address:
127+
'evmosvaloper1sn65acv26jg8kzahlf8gq7cl8pyz3qcpap3fcx',
128+
},
129+
},
130+
],
131+
sequence: '1',
132+
})
133+
})
134+
135+
it('throws on invalid byte payload', () => {
136+
const byteString =
137+
'123 34 97 99 99 111 117 110 116 95 110 117 109 98 101 114 34 58 34 48 34 44 34 99 104 97 105 110 95 105 100 34 58 34 101 118 109 111 115 95 57 48 48 48 45 49 34 44 34 102 101 101 34 58 123 34 97 109 111 117 110 116 34 58 91 123 34 97 109 111 117 110 116 34 58 34 50 48 48 48 34 44 34 100 101 110 111 109 34 58 34 97 101 118 109 111 115 34 125 93 44 34 103 97 115 34 58 34 50 48 48 48 48 48 34 125 44 34 109 101 109 111 34 58 34 34 44 34 109 115 103 115 34 58 91 123 34 116 121 112 101 34 58 34 99 111 115 109 111 115 45 115 100 107 47 77 115 103 68 101 108 101 103 97 116 101 34 44 34 118 97 108 117 101 34 58 123 34 97 109 111 117 110 116 34 58 123 34 97 109 111 117 110 116 34 58 34 49 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 34 44 34 100 101 110 111 109 34 58 34 97 101 118 109 111 115 34 125 44 34 100 101 108 101 103 97 116 111 114 95 97 100 100 114 101 115 115 34 58 34 101 118 109 111 115 49 115 110 54 53 97 99 118 50 54 106 103 56 107 122 97 104 108 102 56 103 113 55 99 108 56 112 121 122 51 113 99 112 115 48 55 101 101 109 34 44 34 118 97 108 105 100 97 116 111 114 95 97 100 100 114 101 115 115 34 58 34 101 118 109 111 115 118 97 108 111 112 101 114 49 115 110 54 53 97 99 118 50 54 106 103 56 107 122 97 104 108 102 56 103 113 55 99 108 56 112 121 122 51 113 99 112 97 112 51 102 99 120 34 125 125 93 44 34 115 101 113 117 101 110 99 101 34 58 34 49 34'
138+
const bytes = Uint8Array.from(byteString.split(' ').map((el) => Number(el)))
139+
expect(() => {
140+
decodeAminoSignDoc(bytes)
141+
}).toThrow(Error)
142+
})
143+
144+
it('fills blank feePayers', () => {
145+
const signDoc = {
146+
account_number: '0',
147+
chain_id: 'evmos_9000-1',
148+
fee: {
149+
amount: [{ amount: '2000', denom: 'aevmos' }],
150+
gas: '200000',
151+
feePayer: '',
152+
},
153+
memo: '',
154+
msgs: [
155+
{
156+
type: 'cosmos-sdk/MsgVote',
157+
value: {
158+
option: 1,
159+
proposal_id: '1',
160+
voter: 'evmos1ygxq25vlp3u4lqyys6vrsdaz9ww9kgrx7xlhty',
161+
},
162+
},
163+
],
164+
sequence: '1',
165+
}
166+
167+
const bytes = Buffer.from(JSON.stringify(signDoc))
168+
const eip712 = decodeAminoSignDoc(bytes)
169+
const message = eip712.message as any
170+
171+
expect(message.fee.feePayer).toBe(
172+
'evmos1ygxq25vlp3u4lqyys6vrsdaz9ww9kgrx7xlhty',
173+
)
174+
})
175+
})
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { parseChainId } from './utils'
2+
import { MSG_VOTE_TYPES } from '../messages/gov'
3+
import { MSG_SEND_TYPES } from '../messages/msgsend'
4+
import { generateTypes, createEIP712 } from '../messages/base'
5+
import { MSG_DELEGATE_TYPES } from '../messages/staking'
6+
7+
export const MSG_TYPES = {
8+
MSG_SEND: 'cosmos-sdk/MsgSend',
9+
MSG_VOTE: 'cosmos-sdk/MsgVote',
10+
MSG_DELEGATE: 'cosmos-sdk/MsgDelegate',
11+
}
12+
13+
// Get the feePayer from the message, using the message structure.
14+
// This is required to provide the feePayer in the EIP712 object, and
15+
// because Amino JS representations are in JSON and have no better interface.
16+
// Throws on error.
17+
export function getFeePayerFromMsg(msg: any) {
18+
switch (msg.type) {
19+
case MSG_TYPES.MSG_SEND:
20+
return msg.value.from_address
21+
case MSG_TYPES.MSG_VOTE:
22+
return msg.value.voter
23+
case MSG_TYPES.MSG_DELEGATE:
24+
return msg.value.delegator_address
25+
default:
26+
throw new Error('Unsupported message type')
27+
}
28+
}
29+
30+
// Return the SignDoc post-formatting. Throws on error.
31+
function formatSignDoc(signDoc: any) {
32+
const signDocCpy: any = {}
33+
Object.assign(signDocCpy, signDoc)
34+
35+
// Fill in the feePayer if the field is blank or unset
36+
if (
37+
!Object.keys(signDoc.fee).includes('feePayer') ||
38+
signDoc.fee.feePayer === ''
39+
) {
40+
signDocCpy.fee.feePayer = getFeePayerFromMsg(signDoc.msgs[0])
41+
}
42+
43+
return signDocCpy
44+
}
45+
46+
// Generate EIP-712 types for the given message
47+
export function eip712MessageType(msg: any) {
48+
switch (msg.type) {
49+
case MSG_TYPES.MSG_SEND:
50+
return generateTypes(MSG_SEND_TYPES)
51+
case MSG_TYPES.MSG_VOTE:
52+
return generateTypes(MSG_VOTE_TYPES)
53+
case MSG_TYPES.MSG_DELEGATE:
54+
return generateTypes(MSG_DELEGATE_TYPES)
55+
default:
56+
throw new Error('Unsupported message type in SignDoc')
57+
}
58+
}
59+
60+
// Decodes the AminoSignDoc to EIP712 types. Throws on error.
61+
export function decodeAminoSignDoc(bytes: Uint8Array) {
62+
const rawSignDoc = JSON.parse(Buffer.from(bytes).toString())
63+
64+
// Enforce single-message signing for now
65+
if (rawSignDoc.msgs.length !== 1) {
66+
throw new Error(
67+
`Expected single message in Amino SignDoc but received ${rawSignDoc.msgs.length}.`,
68+
)
69+
}
70+
71+
// Format SignDoc to match EIP-712 types
72+
const signDoc = formatSignDoc(rawSignDoc)
73+
const chainId = signDoc.chain_id
74+
75+
const msg = signDoc.msgs[0]
76+
const type = eip712MessageType(msg)
77+
78+
return createEIP712(type, parseChainId(chainId), signDoc)
79+
}

0 commit comments

Comments
 (0)