Skip to content

Commit

Permalink
feat: add starknet implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Adegbite Ademola Kelvin authored and Adegbite Ademola Kelvin committed Sep 5, 2024
1 parent eb3eea7 commit 8b32097
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 6,187 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@
"base64url": "^3.0.1",
"bs58": "^4.0.1",
"keccak": "^3.0.2",
"secp256k1": "^5.0.0"
"secp256k1": "^5.0.0",
"starknet": "^6.11.0"
},
"optionalDependencies": {
"@randlabs/myalgo-connect": "^1.1.2",
Expand Down
106 changes: 106 additions & 0 deletions src/__tests__/starknet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
jest.setTimeout(20000);
import StarknetSigner from "../signing/chains/StarknetSigner";
import { RpcProvider } from "starknet";
import Crypto from "crypto";
import { createData } from "../../index";

const tagsTestVariations = [
{ description: "no tags", tags: undefined },
{ description: "empty tags", tags: [] },
{ description: "single tag", tags: [{ name: "Content-Type", value: "image/png" }] },
{
description: "multiple tags",
tags: [
{ name: "Content-Type", value: "image/png" },
{ name: "hello", value: "world" },
{ name: "lorem", value: "ipsum" },
],
},
];

const dataTestVariations = [
{ description: "empty string", data: "" },
{ description: "small string", data: "hello world" },
{ description: "large string", data: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{};':\",./<>?`~" },
{ description: "empty buffer", data: Buffer.from([]) },
{ description: "small buffer", data: Buffer.from("hello world") },
{ description: "large buffer", data: Buffer.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{};':\",./<>?`~") },
];

describe("Typed Starknet Signer", () => {
let signer: StarknetSigner;
const provider = new RpcProvider({ nodeUrl: "https://starknet-sepolia.public.blastapi.io" });

const PrivateKey = "0x0570d0ab0e4bd9735277e8db6c8e19918c64ed50423aa5860235635d2487c7bb";
const myAddressInStarknet = "0x078e47BBEB4Dc687741825d7bEAD044e229960D3362C0C21F45Bb920db08B0c4";

beforeAll(async () => {
signer = new StarknetSigner(provider, myAddressInStarknet, PrivateKey);
});

it("should sign a known value", async () => {
const data = Buffer.from("Hello-world!");
const expectedSignature = Buffer.from([
1, 114, 235, 23, 11, 129, 235, 41, 193, 99, 37, 195, 7, 92, 120, 196, 216, 86, 170, 132, 45, 38, 234, 192, 92, 108, 83, 180, 250, 64, 95, 2, 0,
119, 220, 61, 212, 202, 154, 141, 140, 112, 99, 169, 204, 5, 232, 4, 203, 246, 9, 70, 254, 36, 150, 193, 72, 0, 15, 25, 127, 59, 138, 239, 1,
]);

const signature = await signer.sign(data);
const signatureBuffer = Buffer.from(signature);
expect(signatureBuffer).toEqual(expectedSignature);
});

it("should verify a known values", async () => {
const data = Buffer.from("Hello-world!");
const signature = await signer.sign(data);
const isValid = await StarknetSigner.verify(signer.publicKey, data, signature);
expect(isValid).toEqual(true);
});
it("should sign & verify an unknown value", async () => {
const randData = Crypto.randomBytes(256);
const signature = await signer.sign(randData);
const isValid = await StarknetSigner.verify(signer.publicKey, randData, signature);
expect(isValid).toEqual(true);
});
describe("Create & Validate DataItem", () => {
it("should create a valid dataItem", async () => {
const data = "Hello, Bundlr!";
const tags = [{ name: "Hello", value: "Bundlr" }];
const item = createData(data, signer, { tags });
await item.sign(signer);
expect(await item.isValid()).toBe(true);
});

describe("With an unknown wallet", () => {
it("should sign & verify an unknown value", async () => {
const randSigner = new StarknetSigner(provider, myAddressInStarknet, PrivateKey);
const randData = Crypto.randomBytes(256);
const signature = await randSigner.sign(randData);
const isValid = await StarknetSigner.verify(signer.publicKey, randData, signature);
expect(isValid).toEqual(true);
});
});

describe("and given we want to create a dataItem", () => {
describe.each(tagsTestVariations)("with $description tags", ({ tags }) => {
describe.each(dataTestVariations)("and with $description data", ({ data }) => {
it("should create a valid dataItem", async () => {
const item = createData(data, signer, { tags });
await item.sign(signer);
expect(await item.isValid()).toBe(true);
});
it("should set the correct tags", async () => {
const item = createData(data, signer, { tags });
await item.sign(signer);
expect(item.tags).toEqual(tags ?? []);
});
it("should set the correct data", async () => {
const item = createData(data, signer, { tags });
await item.sign(signer);
expect(item.rawData).toEqual(Buffer.from(data));
});
});
});
});
});
});
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum SignatureConfig {
INJECTEDAPTOS = 5,
MULTIAPTOS = 6,
TYPEDETHEREUM = 7,
STARKNET = 8,
}

export interface SignatureMeta {
Expand Down Expand Up @@ -50,4 +51,9 @@ export const SIG_CONFIG: Record<SignatureConfig, SignatureMeta> = {
pubLength: 42,
sigName: "typedEthereum",
},
[SignatureConfig.STARKNET]:{
sigLength:65,
pubLength: 32,
sigName:'starknet'
}
};
132 changes: 132 additions & 0 deletions src/signing/chains/StarknetSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
Account,
RpcProvider,
Signature,
WeierstrassSignatureType,
ec,
encode,
typedData,
Signer as StarknetSignerAlias,
TypedData,
TypedDataRevision,
} from "starknet";
import type { Signer } from "../index";
import { SignatureConfig, SIG_CONFIG } from "../../constants";

export default class StarknetSigner implements Signer {
protected signer: Account;
public publicKey: Buffer;
public static address: string;
private static privateKey: string;
public static provider: RpcProvider;
public chainId: string;
readonly ownerLength: number = SIG_CONFIG[SignatureConfig.STARKNET].pubLength;
readonly signatureLength: number = SIG_CONFIG[SignatureConfig.STARKNET].sigLength;
readonly signatureType: number = SignatureConfig.STARKNET;

// Constructor to set static properties
constructor(provider: RpcProvider, address: string, pKey: string) {
StarknetSigner.provider = provider;
StarknetSigner.address = address;
StarknetSigner.privateKey = pKey;
this.signer = new Account(provider, address, pKey);
}

public async init() {
try {
const signer = new StarknetSignerAlias(StarknetSigner.privateKey);
const pub_key = await signer.getPubKey();
let hexKey = pub_key.startsWith("0x") ? pub_key.slice(2) : pub_key;
this.publicKey = Buffer.from(0 + hexKey, "hex");
this.chainId = await StarknetSigner.provider.getChainId();
} catch (error) {
console.error("Error setting public key or chain ID:", error);
}
}

async sign(message: Uint8Array, _opts?: any): Promise<Uint8Array> {
if (!this.publicKey) {
this.init();
}
if (!this.signer.signMessage) throw new Error("Selected signer does not support message signing");
let chainId = await this.signer.getChainId();
let message_to_felt = convertToFelt252(message);
let TypedDataMessage = typed_domain({ chainId: chainId, message: message_to_felt });
let signature: Signature = (await this.signer.signMessage(TypedDataMessage.typemessage)) as WeierstrassSignatureType;

const r = BigInt(signature.r).toString(16).padStart(64, "0");
const s = BigInt(signature.s).toString(16).padStart(64, "0");
// @ts-ignore
const recovery = signature.recovery.toString(16).padStart(2, "0");

const rArray = Uint8Array.from(Buffer.from(r, "hex"));
const sArray = Uint8Array.from(Buffer.from(s, "hex"));
const recoveryArray = Uint8Array.from(Buffer.from(recovery, "hex"));

const result = new Uint8Array(rArray.length + sArray.length + recoveryArray.length);
result.set(rArray);
result.set(sArray, rArray.length);
result.set(recoveryArray, rArray.length + sArray.length);

return result;
}

static async verify(_pk: Buffer, message: Uint8Array, _signature: Uint8Array, _opts?: any): Promise<boolean> {
let chainId = await this.provider.getChainId();
let message_to_felt = convertToFelt252(message);
let TypedDataMessage = typed_domain({ chainId, message: message_to_felt });

const fullPubKey = encode.addHexPrefix(encode.buf2hex(ec.starkCurve.getPublicKey(StarknetSigner.privateKey, true)));

const msgHash = typedData.getMessageHash(TypedDataMessage.typemessage, StarknetSigner.address);

return ec.starkCurve.verify(_signature.slice(0, -1), msgHash, fullPubKey);
}
}

// Utility function to convert Uint8Array to felt252
function convertToFelt252(data: Uint8Array): bigint[] {
const felt252Array: bigint[] = [];
const felt252Size = 31;

for (let i = 0; i < data.length; i += felt252Size) {
let value = BigInt(0);
for (let j = 0; j < felt252Size && i + j < data.length; j++) {
value = (value << BigInt(8)) | BigInt(data[i + j]);
}
felt252Array.push(value);
}

return felt252Array;
}

interface TypedParam {
chainId: string | number;
message: bigint[];
}

export const typed_domain = ({ chainId, message }: TypedParam): { typemessage: TypedData } => {
const typemessage: TypedData = {
domain: {
name: "Arbundle",
chainId: chainId,
version: "1.0.2",
revision: TypedDataRevision.ACTIVE,
},
message: {
message,
},
primaryType: "Message",
types: {
Message: [{ name: "message", type: "felt*" }],
StarknetDomain: [
{ name: "name", type: "string" },
{ name: "chainId", type: "felt" },
{ name: "version", type: "string" },
],
},
};
return {
typemessage,
};
};
1 change: 1 addition & 0 deletions src/signing/chains/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export { default as MultiSignatureAptosSigner } from "./multiSignatureAptos";
export { default as TypedEthereumSigner } from "./TypedEthereumSigner";
export * from "./InjectedTypedEthereumSigner";
export { default as ArconnectSigner } from "./arconnectSigner";
export { default as StarknetSigner } from "./StarknetSigner";
5 changes: 5 additions & 0 deletions src/signing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
InjectedAptosSigner,
MultiSignatureAptosSigner,
TypedEthereumSigner,
StarknetSigner
} from "./chains/index";

export type IndexToType = Record<
Expand Down Expand Up @@ -42,4 +43,8 @@ export const indexToType: IndexToType = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
7: TypedEthereumSigner,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
8: StarknetSigner

};
Loading

0 comments on commit 8b32097

Please sign in to comment.