Skip to content

Commit a1e0838

Browse files
authored
Merge pull request #87 from seanjameshan/feature/typed-structured-data-hashing-and-signing
Feature/typed structured data hashing and signing
2 parents e4ecff8 + 4cd969f commit a1e0838

File tree

17 files changed

+461
-32
lines changed

17 files changed

+461
-32
lines changed

__tests__/account.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ describe('deploy and test Wallet', () => {
104104
)
105105
);
106106

107-
const { r, s } = ec.sign(starkKeyPair, msgHash);
107+
const signature = ec.sign(starkKeyPair, msgHash);
108108
const { code, transaction_hash } = await wallet.invoke(
109109
'execute',
110110
{
@@ -113,7 +113,7 @@ describe('deploy and test Wallet', () => {
113113
calldata: [erc20Address, '10'],
114114
nonce: nonce.toString(),
115115
},
116-
[number.toHex(r), number.toHex(s)]
116+
signature
117117
);
118118

119119
expect(code).toBe('TRANSACTION_RECEIVED');
@@ -151,7 +151,7 @@ test('build tx', async () => {
151151
.toString()
152152
);
153153

154-
const { r, s } = ec.sign(keyPair, msgHash);
154+
const [r, s] = ec.sign(keyPair, msgHash);
155155
expect(r.toString()).toBe(
156156
'706800951915233622090196542158919402159816118214143837213294331713137614072'
157157
);

__tests__/utils/ellipticalCurve.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ test('hashMessage()', () => {
4040
);
4141
expect(hashMsg).toBe('0x7f15c38ea577a26f4f553282fcfe4f1feeb8ecfaad8f221ae41abf8224cbddd');
4242
const keyPair = getKeyPair(privateKey);
43-
const { r, s } = sign(keyPair, removeHexPrefix(hashMsg));
43+
const [r, s] = sign(keyPair, removeHexPrefix(hashMsg));
4444
expect(r.toString()).toStrictEqual(
4545
toBN('2458502865976494910213617956670505342647705497324144349552978333078363662855').toString()
4646
);

__tests__/utils/typedData.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { encodeType, getMessageHash, getStructHash, getTypeHash } from '../../src/utils/typedData';
2+
3+
const typedDataExample = {
4+
types: {
5+
StarkNetDomain: [
6+
{ name: 'name', type: 'felt' },
7+
{ name: 'version', type: 'felt' },
8+
{ name: 'chainId', type: 'felt' },
9+
],
10+
Person: [
11+
{ name: 'name', type: 'felt' },
12+
{ name: 'wallet', type: 'felt' },
13+
],
14+
Mail: [
15+
{ name: 'from', type: 'Person' },
16+
{ name: 'to', type: 'Person' },
17+
{ name: 'contents', type: 'felt' },
18+
],
19+
},
20+
primaryType: 'Mail',
21+
domain: {
22+
name: 'StarkNet Mail',
23+
version: '1',
24+
chainId: 1,
25+
},
26+
message: {
27+
from: {
28+
name: 'Cow',
29+
wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
30+
},
31+
to: {
32+
name: 'Bob',
33+
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
34+
},
35+
contents: 'Hello, Bob!',
36+
},
37+
};
38+
39+
describe('typedData', () => {
40+
test('should get right type encoding', () => {
41+
const typeEncoding = encodeType(typedDataExample, 'Mail');
42+
expect(typeEncoding).toMatchInlineSnapshot(
43+
`"Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)"`
44+
);
45+
});
46+
test('should get right type hash', () => {
47+
const typeHashDomain = getTypeHash(typedDataExample, 'StarkNetDomain');
48+
expect(typeHashDomain).toMatchInlineSnapshot(
49+
`"0x1bfc207425a47a5dfa1a50a4f5241203f50624ca5fdf5e18755765416b8e288"`
50+
);
51+
const typeHashPerson = getTypeHash(typedDataExample, 'Person');
52+
expect(typeHashPerson).toMatchInlineSnapshot(
53+
`"0x2896dbe4b96a67110f454c01e5336edc5bbc3635537efd690f122f4809cc855"`
54+
);
55+
const typeHashMail = getTypeHash(typedDataExample, 'Mail');
56+
expect(typeHashMail).toMatchInlineSnapshot(
57+
`"0x13d89452df9512bf750f539ba3001b945576243288137ddb6c788457d4b2f79"`
58+
);
59+
});
60+
test('should get right hash for StarkNetDomain', () => {
61+
const hash = getStructHash(typedDataExample, 'StarkNetDomain', typedDataExample.domain as any);
62+
expect(hash).toMatchInlineSnapshot(
63+
`"0x54833b121883a3e3aebff48ec08a962f5742e5f7b973469c1f8f4f55d470b07"`
64+
);
65+
});
66+
test('should get right hash for entire message', () => {
67+
const hash = getMessageHash(typedDataExample, '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826');
68+
expect(hash).toMatchInlineSnapshot(
69+
`"0x6fcff244f63e38b9d88b9e3378d44757710d1b244282b435cb472053c8d78d0"`
70+
);
71+
});
72+
});

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"json-bigint": "^1.0.0",
6969
"minimalistic-assert": "^1.0.1",
7070
"pako": "^2.0.4",
71+
"superstruct": "^0.15.3",
7172
"url-join": "^4.0.1"
7273
},
7374
"lint-staged": {

src/contract.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import BN from 'bn.js';
22
import assert from 'minimalistic-assert';
33

44
import { Provider, defaultProvider } from './provider';
5-
import { Abi, AbiEntry, FunctionAbi, StructAbi } from './types';
5+
import { Abi, AbiEntry, FunctionAbi, Signature, StructAbi } from './types';
66
import { BigNumberish, toBN } from './utils/number';
77
import { getSelectorFromName } from './utils/stark';
88

@@ -146,7 +146,7 @@ export class Contract {
146146
return this.parseResponseField(methodAbi, responseIterator);
147147
}
148148

149-
public invoke(method: string, args: Args = {}, signature?: [BigNumberish, BigNumberish]) {
149+
public invoke(method: string, args: Args = {}, signature?: Signature) {
150150
// ensure contract is connected
151151
assert(this.connectedTo !== null, 'contract isnt connected to an address');
152152

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export * as stark from './utils/stark';
1818
export * as ec from './utils/ellipticCurve';
1919
export * as uint256 from './utils/uint256';
2020
export * as shortString from './utils/shortString';
21+
export * as typedData from './utils/typedData';

src/provider/default.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
GetContractAddressesResponse,
1212
GetTransactionResponse,
1313
GetTransactionStatusResponse,
14+
Signature,
1415
Transaction,
1516
} from '../types';
1617
import { parse, stringify } from '../utils/json';
@@ -265,7 +266,7 @@ export class Provider implements ProviderInterface {
265266
contractAddress: string,
266267
entrypointSelector: string,
267268
calldata?: string[],
268-
signature?: [BigNumberish, BigNumberish]
269+
signature?: Signature
269270
): Promise<AddTransactionResponse> {
270271
return this.addTransaction({
271272
type: 'INVOKE_FUNCTION',
@@ -278,16 +279,15 @@ export class Provider implements ProviderInterface {
278279

279280
public async waitForTx(txHash: BigNumberish, retryInterval: number = 8000) {
280281
let onchain = false;
282+
await wait(retryInterval);
283+
281284
while (!onchain) {
282285
// eslint-disable-next-line no-await-in-loop
283286
await wait(retryInterval);
284287
// eslint-disable-next-line no-await-in-loop
285288
const res = await this.getTransactionStatus(txHash);
286289

287-
if (
288-
res.tx_status === 'ACCEPTED_ONCHAIN' ||
289-
(res.tx_status === 'PENDING' && res.block_hash !== 'pending') // This is needed as of today. In the future there will be a different status for pending transactions.
290-
) {
290+
if (res.tx_status === 'ACCEPTED_ON_L1' || res.tx_status === 'ACCEPTED_ON_L2') {
291291
onchain = true;
292292
} else if (res.tx_status === 'REJECTED') {
293293
throw Error('REJECTED');

src/provider/interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
GetContractAddressesResponse,
99
GetTransactionResponse,
1010
GetTransactionStatusResponse,
11+
Signature,
1112
Transaction,
1213
} from '../types';
1314
import type { BigNumberish } from '../utils/number';
@@ -135,7 +136,7 @@ export abstract class ProviderInterface {
135136
contractAddress: string,
136137
entrypointSelector: string,
137138
calldata?: string[],
138-
signature?: [BigNumberish, BigNumberish]
139+
signature?: Signature
139140
): Promise<AddTransactionResponse>;
140141

141142
public abstract waitForTx(txHash: BigNumberish, retryInterval?: number): Promise<void>;

src/signer/default.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import assert from 'minimalistic-assert';
22

33
import { Provider } from '../provider';
4-
import { AddTransactionResponse, KeyPair, Transaction } from '../types';
4+
import { AddTransactionResponse, KeyPair, Signature, Transaction } from '../types';
55
import { sign } from '../utils/ellipticCurve';
66
import { addHexPrefix } from '../utils/encode';
77
import { hashMessage } from '../utils/hash';
88
import { toBN } from '../utils/number';
99
import { getSelectorFromName } from '../utils/stark';
10+
import { TypedData, getMessageHash } from '../utils/typedData';
1011
import { SignerInterface } from './interface';
1112

1213
export class Signer extends Provider implements SignerInterface {
@@ -59,7 +60,7 @@ export class Signer extends Provider implements SignerInterface {
5960
)
6061
);
6162

62-
const { r, s } = sign(this.keyPair, msgHash);
63+
const signature = sign(this.keyPair, msgHash);
6364

6465
return super.addTransaction({
6566
type: 'INVOKE_FUNCTION',
@@ -72,7 +73,29 @@ export class Signer extends Provider implements SignerInterface {
7273
nonceBn.toString(),
7374
].map((x) => toBN(x).toString()),
7475
contract_address: this.address,
75-
signature: [r, s],
76+
signature,
7677
});
7778
}
79+
80+
/**
81+
* Sign an JSON object with the starknet private key and return the signature
82+
*
83+
* @param json - JSON object to be signed
84+
* @returns the signature of the JSON object
85+
* @throws {Error} if the JSON object is not a valid JSON
86+
*/
87+
public async signMessage(typedData: TypedData): Promise<Signature> {
88+
return sign(this.keyPair, await this.hashMessage(typedData));
89+
}
90+
91+
/**
92+
* Hash a JSON object with pederson hash and return the hash
93+
*
94+
* @param json - JSON object to be hashed
95+
* @returns the hash of the JSON object
96+
* @throws {Error} if the JSON object is not a valid JSON
97+
*/
98+
public async hashMessage(typedData: TypedData): Promise<string> {
99+
return getMessageHash(typedData, this.address);
100+
}
78101
}

0 commit comments

Comments
 (0)