Skip to content

Commit 12fa1bc

Browse files
authored
Merge pull request #898 from PhilippeR26/EthSigner
new feature: Ethereum signer
2 parents 0f8b266 + f37bfb2 commit 12fa1bc

File tree

6 files changed

+306
-0
lines changed

6 files changed

+306
-0
lines changed

__tests__/utils/ethSigner.test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import typedDataExample from '../../__mocks__/typedDataExample.json';
2+
import {
3+
Call,
4+
DeclareSignerDetails,
5+
DeployAccountSignerDetails,
6+
EthSigner,
7+
InvocationsSignerDetails,
8+
RPC,
9+
constants,
10+
eth,
11+
num,
12+
stark,
13+
} from '../../src';
14+
15+
describe('Ethereum signatures', () => {
16+
describe('privk, pubK', () => {
17+
test('Generates random PK', () => {
18+
const privK = eth.ethRandomPrivateKey();
19+
expect(privK.length).toBe(66);
20+
expect(num.isHex(privK)).toBe(true);
21+
});
22+
23+
test('Generates pubKey', async () => {
24+
const mySigner = new EthSigner(
25+
'0x525bc68475c0955fae83869beec0996114d4bb27b28b781ed2a20ef23121b8de'
26+
);
27+
expect(await mySigner.getPubKey()).toBe(
28+
'0x020178bb97615b49070eefad71cb2f159392274404e41db748d9397147cb25cf59'
29+
);
30+
});
31+
});
32+
33+
describe('Signatures', () => {
34+
test('Message signature', async () => {
35+
const myPrivateKey = '0x525bc68475c0955fae83869beec0996114d4bb27b28b781ed2a20ef23121b8de';
36+
const myEthSigner = new EthSigner(myPrivateKey);
37+
const message = typedDataExample;
38+
const sig = await myEthSigner.signMessage(
39+
message,
40+
'0x65a822fbee1ae79e898688b5a4282dc79e0042cbed12f6169937fddb4c26641'
41+
);
42+
expect(sig).toMatchObject({
43+
r: 46302720252787165203319064060867586811009528414735725622252684979112343882634n,
44+
s: 44228007167516598548621407232357037139087111723794788802261070080184864735744n,
45+
recovery: 1,
46+
});
47+
});
48+
49+
// TODO : To update when a contract account handling ETHEREUM signatures will be available.
50+
test('Transaction signature', async () => {
51+
const myPrivateKey = '0x525bc68475c0955fae83869beec0996114d4bb27b28b781ed2a20ef23121b8de';
52+
const myEthSigner = new EthSigner(myPrivateKey);
53+
const myCall: Call = {
54+
contractAddress: '0x65a822fbee1ae79e898688b5a4282dc79e0042cbed12f6169937fddb4c26641',
55+
entrypoint: 'test',
56+
calldata: [1, 2],
57+
};
58+
const sig = await myEthSigner.signTransaction([myCall], {
59+
version: '0x2',
60+
walletAddress: '0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691',
61+
cairoVersion: '1',
62+
chainId: constants.StarknetChainId.SN_SEPOLIA,
63+
nonce: 45,
64+
maxFee: 10 ** 15,
65+
} as InvocationsSignerDetails);
66+
expect(sig).toMatchObject({
67+
r: 7985353442887841088086521795914083018399735702575968460096442990678259802335n,
68+
s: 54448706138210541940611627632626053501325595041277792020051079616748389329289n,
69+
recovery: 0,
70+
});
71+
});
72+
73+
test('Deploy account signature', async () => {
74+
const myPrivateKey = '0x525bc68475c0955fae83869beec0996114d4bb27b28b781ed2a20ef23121b8de';
75+
const myEthSigner = new EthSigner(myPrivateKey);
76+
const myDeployAcc: DeployAccountSignerDetails = {
77+
version: '0x2',
78+
contractAddress: '0x65a822fbee1ae79e898688b5a4282dc79e0042cbed12f6169937fddb4c26641',
79+
chainId: constants.StarknetChainId.SN_SEPOLIA,
80+
classHash: '0x5f3614e8671257aff9ac38e929c74d65b02d460ae966cd826c9f04a7fa8e0d4',
81+
constructorCalldata: [1, 2],
82+
addressSalt: 1234,
83+
nonce: 45,
84+
maxFee: 10 ** 15,
85+
86+
tip: 0,
87+
paymasterData: [],
88+
accountDeploymentData: [],
89+
nonceDataAvailabilityMode: RPC.EDataAvailabilityMode.L1,
90+
feeDataAvailabilityMode: RPC.EDataAvailabilityMode.L1,
91+
resourceBounds: stark.estimateFeeToBounds(constants.ZERO),
92+
};
93+
const sig = await myEthSigner.signDeployAccountTransaction(myDeployAcc);
94+
expect(sig).toMatchObject({
95+
r: 61114347636551792612206610795983058940674613154346642566929862226007498517027n,
96+
s: 38870792724053768239218215863749216579253019684549941316832072720775828116206n,
97+
recovery: 1,
98+
});
99+
});
100+
101+
test('Declare signature', async () => {
102+
const myPrivateKey = '0x525bc68475c0955fae83869beec0996114d4bb27b28b781ed2a20ef23121b8de';
103+
const myEthSigner = new EthSigner(myPrivateKey);
104+
const myDeclare: DeclareSignerDetails = {
105+
version: '0x2',
106+
chainId: constants.StarknetChainId.SN_SEPOLIA,
107+
senderAddress: '0x65a822fbee1ae79e898688b5a4282dc79e0042cbed12f6169937fddb4c26641',
108+
classHash: '0x5f3614e8671257aff9ac38e929c74d65b02d460ae966cd826c9f04a7fa8e0d4',
109+
nonce: 45,
110+
maxFee: 10 ** 15,
111+
112+
tip: 0,
113+
paymasterData: [],
114+
accountDeploymentData: [],
115+
nonceDataAvailabilityMode: RPC.EDataAvailabilityMode.L1,
116+
feeDataAvailabilityMode: RPC.EDataAvailabilityMode.L1,
117+
resourceBounds: stark.estimateFeeToBounds(constants.ZERO),
118+
};
119+
const sig = await myEthSigner.signDeclareTransaction(myDeclare);
120+
expect(sig).toMatchObject({
121+
r: 38069596217315916583476609659691868035000959604311196895707605245620900872129n,
122+
s: 420191492562045858770062885997406552542950984883779606809355688615026963844n,
123+
recovery: 1,
124+
});
125+
});
126+
});
127+
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * as json from './utils/json';
2323
export * as num from './utils/num';
2424
export * as transaction from './utils/transaction';
2525
export * as stark from './utils/stark';
26+
export * as eth from './utils/eth';
2627
export * as merkle from './utils/merkle';
2728
export * as uint256 from './utils/uint256';
2829
export * as shortString from './utils/shortString';

src/signer/ethSigner.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { secp256k1 } from '@noble/curves/secp256k1';
2+
3+
import {
4+
Call,
5+
DeclareSignerDetails,
6+
DeployAccountSignerDetails,
7+
InvocationsSignerDetails,
8+
Signature,
9+
TypedData,
10+
V2DeclareSignerDetails,
11+
V2DeployAccountSignerDetails,
12+
V2InvocationsSignerDetails,
13+
V3DeclareSignerDetails,
14+
V3DeployAccountSignerDetails,
15+
V3InvocationsSignerDetails,
16+
} from '../types';
17+
import { ETransactionVersion2, ETransactionVersion3 } from '../types/api';
18+
import { CallData } from '../utils/calldata';
19+
import { addHexPrefix, buf2hex, removeHexPrefix, sanitizeHex } from '../utils/encode';
20+
import { ethRandomPrivateKey } from '../utils/eth';
21+
import {
22+
calculateDeclareTransactionHash,
23+
calculateDeployAccountTransactionHash,
24+
calculateInvokeTransactionHash,
25+
} from '../utils/hash';
26+
import { toHex } from '../utils/num';
27+
import { intDAM } from '../utils/stark';
28+
import { getExecuteCalldata } from '../utils/transaction';
29+
import { getMessageHash } from '../utils/typedData';
30+
import { SignerInterface } from './interface';
31+
32+
/**
33+
* Signer for accounts using Ethereum signature
34+
*/
35+
export class EthSigner implements SignerInterface {
36+
protected pk: string; // hex string without 0x and odd number of characters
37+
38+
constructor(pk: Uint8Array | string = ethRandomPrivateKey()) {
39+
this.pk =
40+
pk instanceof Uint8Array
41+
? removeHexPrefix(sanitizeHex(buf2hex(pk)))
42+
: removeHexPrefix(sanitizeHex(toHex(pk)));
43+
}
44+
45+
public async getPubKey(): Promise<string> {
46+
return addHexPrefix(buf2hex(secp256k1.getPublicKey(this.pk)));
47+
}
48+
49+
public async signMessage(typedData: TypedData, accountAddress: string): Promise<Signature> {
50+
const msgHash = getMessageHash(typedData, accountAddress);
51+
return secp256k1.sign(removeHexPrefix(sanitizeHex(msgHash)), this.pk);
52+
}
53+
54+
public async signTransaction(
55+
transactions: Call[],
56+
details: InvocationsSignerDetails
57+
): Promise<Signature> {
58+
const compiledCalldata = getExecuteCalldata(transactions, details.cairoVersion);
59+
let msgHash;
60+
61+
// TODO: How to do generic union discriminator for all like this
62+
if (Object.values(ETransactionVersion2).includes(details.version as any)) {
63+
const det = details as V2InvocationsSignerDetails;
64+
msgHash = calculateInvokeTransactionHash({
65+
...det,
66+
senderAddress: det.walletAddress,
67+
compiledCalldata,
68+
version: det.version,
69+
});
70+
} else if (Object.values(ETransactionVersion3).includes(details.version as any)) {
71+
const det = details as V3InvocationsSignerDetails;
72+
msgHash = calculateInvokeTransactionHash({
73+
...det,
74+
senderAddress: det.walletAddress,
75+
compiledCalldata,
76+
version: det.version,
77+
nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode),
78+
feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode),
79+
});
80+
} else {
81+
throw Error('unsupported signTransaction version');
82+
}
83+
84+
return secp256k1.sign(removeHexPrefix(sanitizeHex(msgHash)), this.pk);
85+
}
86+
87+
public async signDeployAccountTransaction(
88+
details: DeployAccountSignerDetails
89+
): Promise<Signature> {
90+
const compiledConstructorCalldata = CallData.compile(details.constructorCalldata);
91+
/* const version = BigInt(details.version).toString(); */
92+
let msgHash;
93+
94+
if (Object.values(ETransactionVersion2).includes(details.version as any)) {
95+
const det = details as V2DeployAccountSignerDetails;
96+
msgHash = calculateDeployAccountTransactionHash({
97+
...det,
98+
salt: det.addressSalt,
99+
constructorCalldata: compiledConstructorCalldata,
100+
version: det.version,
101+
});
102+
} else if (Object.values(ETransactionVersion3).includes(details.version as any)) {
103+
const det = details as V3DeployAccountSignerDetails;
104+
msgHash = calculateDeployAccountTransactionHash({
105+
...det,
106+
salt: det.addressSalt,
107+
compiledConstructorCalldata,
108+
version: det.version,
109+
nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode),
110+
feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode),
111+
});
112+
} else {
113+
throw Error('unsupported signDeployAccountTransaction version');
114+
}
115+
116+
return secp256k1.sign(removeHexPrefix(sanitizeHex(msgHash)), this.pk);
117+
}
118+
119+
public async signDeclareTransaction(
120+
// contractClass: ContractClass, // Should be used once class hash is present in ContractClass
121+
details: DeclareSignerDetails
122+
): Promise<Signature> {
123+
let msgHash;
124+
125+
if (Object.values(ETransactionVersion2).includes(details.version as any)) {
126+
const det = details as V2DeclareSignerDetails;
127+
msgHash = calculateDeclareTransactionHash({
128+
...det,
129+
version: det.version,
130+
});
131+
} else if (Object.values(ETransactionVersion3).includes(details.version as any)) {
132+
const det = details as V3DeclareSignerDetails;
133+
msgHash = calculateDeclareTransactionHash({
134+
...det,
135+
version: det.version,
136+
nonceDataAvailabilityMode: intDAM(det.nonceDataAvailabilityMode),
137+
feeDataAvailabilityMode: intDAM(det.feeDataAvailabilityMode),
138+
});
139+
} else {
140+
throw Error('unsupported signDeclareTransaction version');
141+
}
142+
143+
return secp256k1.sign(removeHexPrefix(sanitizeHex(msgHash)), this.pk);
144+
}
145+
}

src/signer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './interface';
22
export * from './default';
3+
export * from './ethSigner';

src/utils/eth.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { secp256k1 } from '@noble/curves/secp256k1';
2+
3+
import { buf2hex, sanitizeHex } from './encode';
4+
5+
/**
6+
* Get random Ethereum private Key.
7+
* @returns an Hex string
8+
* @example
9+
* const myPK: string = randomAddress()
10+
* // result = "0xf04e69ac152fba37c02929c2ae78c9a481461dda42dbc6c6e286be6eb2a8ab83"
11+
*/
12+
export function ethRandomPrivateKey(): string {
13+
return sanitizeHex(buf2hex(secp256k1.utils.randomPrivateKey()));
14+
}

www/docs/guides/connect_account.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,21 @@ const account = new Account(provider, accountAddress, privateKey);
7777
// add ,"1" after privateKey if this account is not a Cairo 0 contract
7878

7979
```
80+
81+
## Connect to an account that uses Ethereum signature
82+
83+
As a consequence of account abstraction, you can find accounts that uses Ethereum signature logical.
84+
To connect to this type of account:
85+
86+
```typescript
87+
const myEthPrivateKey = "0x525bc68475c0955fae83869beec0996114d4bb27b28b781ed2a20ef23121b8de";
88+
const myEthAccountAddress = "0x65a822fbee1ae79e898688b5a4282dc79e0042cbed12f6169937fddb4c26641";
89+
const myEthSigner = new EthSigner(myEthPrivateKey);
90+
const myEthAccount = new Account(provider, myEthAccountAddress, myEthSigner)
91+
```
92+
93+
And if you need a randon Ethereum private key:
94+
95+
```typescript
96+
const myPrivateKey = eth.ethRandomPrivateKey();
97+
```

0 commit comments

Comments
 (0)