Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented the Message and MessageComputer classes #378

Merged
merged 6 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const CONTRACT_DEPLOY_ADDRESS = "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
export const DELEGATION_MANAGER_SC_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6";
export const DEFAULT_HRP = "erd";
export const ESDT_CONTRACT_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u";
export const DEFAULT_MESSAGE_VERSION = 1;
export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n";
51 changes: 40 additions & 11 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,45 @@ export interface IPlainTransactionObject {
guardianSignature?: string;
}

export interface ISignature { hex(): string; }
export interface IAddress { bech32(): string; }
export interface ITransactionValue { toString(): string; }
export interface IAccountBalance { toString(): string; }
export interface INonce { valueOf(): number; }
export interface IChainID { valueOf(): string; }
export interface IGasLimit { valueOf(): number; }
export interface IGasPrice { valueOf(): number; }
export interface ITransactionVersion { valueOf(): number; }
export interface ITransactionOptions { valueOf(): number; }
export interface ISignature {
hex(): string;
}

export interface IAddress {
bech32(): string;
}

export interface ITransactionValue {
toString(): string;
}

export interface IAccountBalance {
toString(): string;
}

export interface INonce {
valueOf(): number;
}

export interface IChainID {
valueOf(): string;
}

export interface IGasLimit {
valueOf(): number;
}

export interface IGasPrice {
valueOf(): number;
}

export interface ITransactionVersion {
valueOf(): number;
}

export interface ITransactionOptions {
valueOf(): number;
}

export interface ITransactionPayload {
length(): number;
Expand Down Expand Up @@ -72,4 +101,4 @@ export interface ITransactionNext {
guardian: string;
signature: Uint8Array;
guardianSignature: Uint8Array;
}
}
87 changes: 87 additions & 0 deletions src/message.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { assert } from "chai";
import { Message, MessageComputer } from "./message";
import { loadTestWallets, TestWallet } from "./testutils";
import { UserVerifier } from "@multiversx/sdk-wallet";
import { DEFAULT_MESSAGE_VERSION } from "./constants";

describe("test message", () => {
let alice: TestWallet;
const messageComputer = new MessageComputer();

before(async function () {
({ alice } = await loadTestWallets());
});

it("should test message compute bytes for signing", async () => {
const data = Buffer.from("test message");

const message = new Message({
data: data,
});

const serialized = messageComputer.computeBytesForSigning(message);

assert.equal(
Buffer.from(serialized).toString("hex"),
"2162d6271208429e6d3e664139e98ba7c5f1870906fb113e8903b1d3f531004d",
);
});

it("should create, sign, pack, unpack and verify message", async () => {
const data = Buffer.from("test");

const message = new Message({
data: data,
address: alice.getAddress(),
});

message.signature = await alice.signer.sign(Buffer.from(messageComputer.computeBytesForSigning(message)));

assert.equal(
Buffer.from(message.signature).toString("hex"),
"7aff43cd6e3d880a65033bf0a1b16274854fd7dfa9fe5faa7fa9a665ee851afd4c449310f5f1697d348e42d1819eaef69080e33e7652d7393521ed50d7427a0e",
);

const packedMessage = messageComputer.packMessage(message);
assert.deepEqual(packedMessage, {
address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
message: "74657374",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And now, re-re-thinking about it 🙈

@ccorcoveanu, all right if we do it this way? That is, without the 0x prefix. Seems less cumbersome. And when people see signature they automatically think: well, that is hex, of course. And message is the same, for consistency. How do you feel about it?

For context: #375

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the no-0x version better

signature:
"7aff43cd6e3d880a65033bf0a1b16274854fd7dfa9fe5faa7fa9a665ee851afd4c449310f5f1697d348e42d1819eaef69080e33e7652d7393521ed50d7427a0e",
version: 1,
});

const unpackedMessage = messageComputer.unpackMessage(packedMessage);
assert.deepEqual(unpackedMessage.address, alice.getAddress());
assert.deepEqual(unpackedMessage.data, message.data);
assert.deepEqual(unpackedMessage.signature, message.signature);
assert.deepEqual(unpackedMessage.version, message.version);

const verifier = UserVerifier.fromAddress(alice.getAddress());
const isValid = verifier.verify(
Buffer.from(messageComputer.computeBytesForVerifying(unpackedMessage)),
Buffer.from(unpackedMessage.signature!),
);
assert.equal(isValid, true);
});

it("should unpack legacy message", async () => {
const legacyMessage = {
address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
message: "0x7468697320697320612074657374206d657373616765",
signature:
"0xb16847437049986f936dd4a0917c869730cbf29e40a0c0821ca70db33f44758c3d41bcbea446dee70dea13d50942343bb78e74979dc434bbb2b901e0f4fd1809",
version: 1,
signer: "ErdJS",
};

const message = messageComputer.unpackMessage(legacyMessage);
assert.deepEqual(message.address, alice.getAddress());
assert.deepEqual(Buffer.from(message.data).toString(), "this is a test message");
assert.deepEqual(
Buffer.from(message.signature!).toString("hex"),
"b16847437049986f936dd4a0917c869730cbf29e40a0c0821ca70db33f44758c3d41bcbea446dee70dea13d50942343bb78e74979dc434bbb2b901e0f4fd1809",
);
assert.deepEqual(message.version, DEFAULT_MESSAGE_VERSION);
});
});
90 changes: 90 additions & 0 deletions src/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { IAddress } from "./interface";
import { DEFAULT_MESSAGE_VERSION, MESSAGE_PREFIX } from "./constants";
import { Address } from "./address";

const createKeccakHash = require("keccak");

Check warning on line 5 in src/message.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Require statement not part of import statement

export class Message {
/**
* Actual message being signed.
*/
public data: Uint8Array;
/**
* The message signature.
*/
public signature?: Uint8Array;
/**
* Address of the wallet that performed the signing operation.
*/
public address?: IAddress;
/**
* Number representing the message version.
*/
public version: number;

constructor(options: { data: Uint8Array; signature?: Uint8Array; address?: IAddress; version?: number }) {
this.data = options.data;
this.signature = options.signature;
this.address = options.address;
this.version = options.version || DEFAULT_MESSAGE_VERSION;
}
}

export class MessageComputer {
constructor() {}

computeBytesForSigning(message: Message): Uint8Array {
const messageSize = Buffer.from(message.data.length.toString());
const signableMessage = Buffer.concat([messageSize, message.data]);
let bytesToHash = Buffer.concat([Buffer.from(MESSAGE_PREFIX), signableMessage]);

return createKeccakHash("keccak256").update(bytesToHash).digest();
}

computeBytesForVerifying(message: Message): Uint8Array {
return this.computeBytesForSigning(message);
}

packMessage(message: Message): {
message: string;
signature: string;
address: string;
version: number;
} {
return {
message: Buffer.from(message.data).toString("hex"),
signature: message.signature ? Buffer.from(message.signature).toString("hex") : "",
address: message.address ? message.address.bech32() : "",
version: message.version ? message.version : DEFAULT_MESSAGE_VERSION,
};
}

unpackMessage(packedMessage: { message: string; signature?: string; address?: string; version?: number }): Message {
const dataHex = this.trimHexPrefix(packedMessage.message);
const data = Buffer.from(dataHex, "hex");

const signatureHex = this.trimHexPrefix(packedMessage.signature || "");
const signature = Buffer.from(signatureHex, "hex");

let address: Address | undefined = undefined;
if (packedMessage.address) {
address = Address.fromBech32(packedMessage.address);
}

const version = packedMessage.version || DEFAULT_MESSAGE_VERSION;

return new Message({
data: data,
signature: signature,
address: address,
version: version,
});
}

private trimHexPrefix(data: string): string {
if (data.startsWith("0x") || data.startsWith("0X")) {
return data.slice(2);
}
return data;
}
}
Loading
Loading