Skip to content

Commit

Permalink
Merge pull request #378 from multiversx/message-and-msg-computer
Browse files Browse the repository at this point in the history
Implemented the Message and MessageComputer classes
  • Loading branch information
popenta committed Feb 20, 2024
2 parents 2647a16 + 2a7ca04 commit 495dcb7
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 85 deletions.
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",
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

0 comments on commit 495dcb7

Please sign in to comment.