Skip to content

Commit

Permalink
Merge pull request #87 from seanjameshan/feature/typed-structured-dat…
Browse files Browse the repository at this point in the history
…a-hashing-and-signing

Feature/typed structured data hashing and signing
  • Loading branch information
janek26 committed Dec 13, 2021
2 parents e4ecff8 + 4cd969f commit a1e0838
Show file tree
Hide file tree
Showing 17 changed files with 461 additions and 32 deletions.
6 changes: 3 additions & 3 deletions __tests__/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('deploy and test Wallet', () => {
)
);

const { r, s } = ec.sign(starkKeyPair, msgHash);
const signature = ec.sign(starkKeyPair, msgHash);
const { code, transaction_hash } = await wallet.invoke(
'execute',
{
Expand All @@ -113,7 +113,7 @@ describe('deploy and test Wallet', () => {
calldata: [erc20Address, '10'],
nonce: nonce.toString(),
},
[number.toHex(r), number.toHex(s)]
signature
);

expect(code).toBe('TRANSACTION_RECEIVED');
Expand Down Expand Up @@ -151,7 +151,7 @@ test('build tx', async () => {
.toString()
);

const { r, s } = ec.sign(keyPair, msgHash);
const [r, s] = ec.sign(keyPair, msgHash);
expect(r.toString()).toBe(
'706800951915233622090196542158919402159816118214143837213294331713137614072'
);
Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils/ellipticalCurve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ test('hashMessage()', () => {
);
expect(hashMsg).toBe('0x7f15c38ea577a26f4f553282fcfe4f1feeb8ecfaad8f221ae41abf8224cbddd');
const keyPair = getKeyPair(privateKey);
const { r, s } = sign(keyPair, removeHexPrefix(hashMsg));
const [r, s] = sign(keyPair, removeHexPrefix(hashMsg));
expect(r.toString()).toStrictEqual(
toBN('2458502865976494910213617956670505342647705497324144349552978333078363662855').toString()
);
Expand Down
72 changes: 72 additions & 0 deletions __tests__/utils/typedData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { encodeType, getMessageHash, getStructHash, getTypeHash } from '../../src/utils/typedData';

const typedDataExample = {
types: {
StarkNetDomain: [
{ name: 'name', type: 'felt' },
{ name: 'version', type: 'felt' },
{ name: 'chainId', type: 'felt' },
],
Person: [
{ name: 'name', type: 'felt' },
{ name: 'wallet', type: 'felt' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'felt' },
],
},
primaryType: 'Mail',
domain: {
name: 'StarkNet Mail',
version: '1',
chainId: 1,
},
message: {
from: {
name: 'Cow',
wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
},
to: {
name: 'Bob',
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
},
contents: 'Hello, Bob!',
},
};

describe('typedData', () => {
test('should get right type encoding', () => {
const typeEncoding = encodeType(typedDataExample, 'Mail');
expect(typeEncoding).toMatchInlineSnapshot(
`"Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)"`
);
});
test('should get right type hash', () => {
const typeHashDomain = getTypeHash(typedDataExample, 'StarkNetDomain');
expect(typeHashDomain).toMatchInlineSnapshot(
`"0x1bfc207425a47a5dfa1a50a4f5241203f50624ca5fdf5e18755765416b8e288"`
);
const typeHashPerson = getTypeHash(typedDataExample, 'Person');
expect(typeHashPerson).toMatchInlineSnapshot(
`"0x2896dbe4b96a67110f454c01e5336edc5bbc3635537efd690f122f4809cc855"`
);
const typeHashMail = getTypeHash(typedDataExample, 'Mail');
expect(typeHashMail).toMatchInlineSnapshot(
`"0x13d89452df9512bf750f539ba3001b945576243288137ddb6c788457d4b2f79"`
);
});
test('should get right hash for StarkNetDomain', () => {
const hash = getStructHash(typedDataExample, 'StarkNetDomain', typedDataExample.domain as any);
expect(hash).toMatchInlineSnapshot(
`"0x54833b121883a3e3aebff48ec08a962f5742e5f7b973469c1f8f4f55d470b07"`
);
});
test('should get right hash for entire message', () => {
const hash = getMessageHash(typedDataExample, '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826');
expect(hash).toMatchInlineSnapshot(
`"0x6fcff244f63e38b9d88b9e3378d44757710d1b244282b435cb472053c8d78d0"`
);
});
});
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"json-bigint": "^1.0.0",
"minimalistic-assert": "^1.0.1",
"pako": "^2.0.4",
"superstruct": "^0.15.3",
"url-join": "^4.0.1"
},
"lint-staged": {
Expand Down
4 changes: 2 additions & 2 deletions src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import BN from 'bn.js';
import assert from 'minimalistic-assert';

import { Provider, defaultProvider } from './provider';
import { Abi, AbiEntry, FunctionAbi, StructAbi } from './types';
import { Abi, AbiEntry, FunctionAbi, Signature, StructAbi } from './types';
import { BigNumberish, toBN } from './utils/number';
import { getSelectorFromName } from './utils/stark';

Expand Down Expand Up @@ -146,7 +146,7 @@ export class Contract {
return this.parseResponseField(methodAbi, responseIterator);
}

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

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * as stark from './utils/stark';
export * as ec from './utils/ellipticCurve';
export * as uint256 from './utils/uint256';
export * as shortString from './utils/shortString';
export * as typedData from './utils/typedData';
10 changes: 5 additions & 5 deletions src/provider/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
GetContractAddressesResponse,
GetTransactionResponse,
GetTransactionStatusResponse,
Signature,
Transaction,
} from '../types';
import { parse, stringify } from '../utils/json';
Expand Down Expand Up @@ -265,7 +266,7 @@ export class Provider implements ProviderInterface {
contractAddress: string,
entrypointSelector: string,
calldata?: string[],
signature?: [BigNumberish, BigNumberish]
signature?: Signature
): Promise<AddTransactionResponse> {
return this.addTransaction({
type: 'INVOKE_FUNCTION',
Expand All @@ -278,16 +279,15 @@ export class Provider implements ProviderInterface {

public async waitForTx(txHash: BigNumberish, retryInterval: number = 8000) {
let onchain = false;
await wait(retryInterval);

while (!onchain) {
// eslint-disable-next-line no-await-in-loop
await wait(retryInterval);
// eslint-disable-next-line no-await-in-loop
const res = await this.getTransactionStatus(txHash);

if (
res.tx_status === 'ACCEPTED_ONCHAIN' ||
(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.
) {
if (res.tx_status === 'ACCEPTED_ON_L1' || res.tx_status === 'ACCEPTED_ON_L2') {
onchain = true;
} else if (res.tx_status === 'REJECTED') {
throw Error('REJECTED');
Expand Down
3 changes: 2 additions & 1 deletion src/provider/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
GetContractAddressesResponse,
GetTransactionResponse,
GetTransactionStatusResponse,
Signature,
Transaction,
} from '../types';
import type { BigNumberish } from '../utils/number';
Expand Down Expand Up @@ -135,7 +136,7 @@ export abstract class ProviderInterface {
contractAddress: string,
entrypointSelector: string,
calldata?: string[],
signature?: [BigNumberish, BigNumberish]
signature?: Signature
): Promise<AddTransactionResponse>;

public abstract waitForTx(txHash: BigNumberish, retryInterval?: number): Promise<void>;
Expand Down
29 changes: 26 additions & 3 deletions src/signer/default.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import assert from 'minimalistic-assert';

import { Provider } from '../provider';
import { AddTransactionResponse, KeyPair, Transaction } from '../types';
import { AddTransactionResponse, KeyPair, Signature, Transaction } from '../types';
import { sign } from '../utils/ellipticCurve';
import { addHexPrefix } from '../utils/encode';
import { hashMessage } from '../utils/hash';
import { toBN } from '../utils/number';
import { getSelectorFromName } from '../utils/stark';
import { TypedData, getMessageHash } from '../utils/typedData';
import { SignerInterface } from './interface';

export class Signer extends Provider implements SignerInterface {
Expand Down Expand Up @@ -59,7 +60,7 @@ export class Signer extends Provider implements SignerInterface {
)
);

const { r, s } = sign(this.keyPair, msgHash);
const signature = sign(this.keyPair, msgHash);

return super.addTransaction({
type: 'INVOKE_FUNCTION',
Expand All @@ -72,7 +73,29 @@ export class Signer extends Provider implements SignerInterface {
nonceBn.toString(),
].map((x) => toBN(x).toString()),
contract_address: this.address,
signature: [r, s],
signature,
});
}

/**
* Sign an JSON object with the starknet private key and return the signature
*
* @param json - JSON object to be signed
* @returns the signature of the JSON object
* @throws {Error} if the JSON object is not a valid JSON
*/
public async signMessage(typedData: TypedData): Promise<Signature> {
return sign(this.keyPair, await this.hashMessage(typedData));
}

/**
* Hash a JSON object with pederson hash and return the hash
*
* @param json - JSON object to be hashed
* @returns the hash of the JSON object
* @throws {Error} if the JSON object is not a valid JSON
*/
public async hashMessage(typedData: TypedData): Promise<string> {
return getMessageHash(typedData, this.address);
}
}
23 changes: 22 additions & 1 deletion src/signer/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Provider } from '../provider';
import { AddTransactionResponse, Transaction } from '../types';
import { AddTransactionResponse, Signature, Transaction } from '../types';
import { TypedData } from '../utils/typedData/types';

export abstract class SignerInterface extends Provider {
public abstract address: string;
Expand All @@ -14,4 +15,24 @@ export abstract class SignerInterface extends Provider {
public abstract override addTransaction(
transaction: Transaction
): Promise<AddTransactionResponse>;

/**
* Sign an JSON object for off-chain usage with the starknet private key and return the signature
* This adds a message prefix so it cant be interchanged with transactions
*
* @param json - JSON object to be signed
* @returns the signature of the JSON object
* @throws {Error} if the JSON object is not a valid JSON
*/
public abstract signMessage(typedData: TypedData): Promise<Signature>;

/**
* Hash a JSON object with pederson hash and return the hash
* This adds a message prefix so it cant be interchanged with transactions
*
* @param json - JSON object to be hashed
* @returns the hash of the JSON object
* @throws {Error} if the JSON object is not a valid JSON
*/
public abstract hashMessage(typedData: TypedData): Promise<string>;
}
12 changes: 9 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import type { ec as EC } from 'elliptic';
import type { BigNumberish } from './utils/number';

export type KeyPair = EC.KeyPair;
export type Signature = EC.Signature;
export type Signature = BigNumberish[];

export type GetContractAddressesResponse = {
Starknet: string;
GpsStatementVerifier: string;
};

export type Status = 'NOT_RECEIVED' | 'RECEIVED' | 'PENDING' | 'REJECTED' | 'ACCEPTED_ONCHAIN';
export type Status =
| 'NOT_RECEIVED'
| 'RECEIVED'
| 'PENDING'
| 'ACCEPTED_ON_L2'
| 'ACCEPTED_ON_L1'
| 'REJECTED';
export type TransactionStatus = 'TRANSACTION_RECEIVED';
export type Type = 'DEPLOY' | 'INVOKE_FUNCTION';
export type EntryPointType = 'EXTERNAL';
Expand Down Expand Up @@ -57,7 +63,7 @@ export type DeployTransaction = {
export type InvokeFunctionTransaction = {
type: 'INVOKE_FUNCTION';
contract_address: string;
signature?: [BigNumberish, BigNumberish];
signature?: Signature;
entry_point_type?: EntryPointType;
entry_point_selector: string;
calldata?: string[];
Expand Down

0 comments on commit a1e0838

Please sign in to comment.